Un registro activo es mapear una entidad de base de datos a un objeto PHP. Hablando en términos simples, si tienes una tabla de usuarios en tu base de datos, puedes "traducir" una fila en esa tabla a una clase User y a un objeto $user en tu base de código. Consulta ejemplo básico.
User
$user
Haz clic aquí para acceder al repositorio en GitHub.
Supongamos que tienes la siguiente tabla:
CREATE TABLE users ( id INTEGER PRIMARY KEY, name TEXT, password TEXT );
Ahora puedes configurar una nueva clase para representar esta tabla:
/** * Una clase ActiveRecord suele ser singular * * Se recomienda encarecidamente agregar las propiedades de la tabla como comentarios aquí * * @property int $id * @property string $name * @property string $password */ class User extends flight\ActiveRecord { public function __construct($database_connection) { // puedes configurarlo así parent::__construct($database_connection, 'users'); // o así parent::__construct($database_connection, null, [ 'table' => 'users']); } }
¡Ahora mira cómo sucede la magia!
// para sqlite $database_connection = new PDO('sqlite:test.db'); // esto es solo un ejemplo, probablemente usarías una conexión a base de datos real // para mysql $database_connection = new PDO('mysql:host=localhost;dbname=test_db&charset=utf8bm4', 'username', 'password'); // o mysqli $database_connection = new mysqli('localhost', 'username', 'password', 'test_db'); // o mysqli con creación no basada en objeto $database_connection = mysqli_connect('localhost', 'username', 'password', 'test_db'); $user = new User($database_connection); $user->name = 'Bobby Tables'; $user->password = password_hash('some cool password'); $user->insert(); // o $user->save(); echo $user->id; // 1 $user->name = 'Joseph Mamma'; $user->password = password_hash('some cool password again!!!'); $user->insert(); // ¡no puedes usar $user->save() aquí o pensará que es una actualización! echo $user->id; // 2
¡Y fue tan fácil añadir un nuevo usuario! Ahora que hay una fila de usuario en la base de datos, ¿cómo la puedes extraer?
$user->find(1); // busca id = 1 en la base de datos y lo devuelve. echo $user->name; // 'Bobby Tables'
¿Y qué pasa si quieres encontrar todos los usuarios?
$users = $user->findAll();
¿Qué tal con una condición específica?
$users = $user->like('name', '%mamma%')->findAll();
¿Ves lo divertido que es esto? ¡Instalémoslo y comencemos!
Simplemente instala con Composer
composer require flightphp/active-record
Esto puede ser utilizado como una biblioteca independiente o con el Flight PHP Framework. Totalmente depende de ti.
Solo asegúrate de pasar una conexión PDO al constructor.
$pdo_connection = new PDO('sqlite:test.db'); // esto es solo un ejemplo, probablemente usarías una conexión a base de datos real $User = new User($pdo_connection);
¿No quieres establecer siempre tu conexión a base de datos en el constructor? Consulta Gestión de Conexiones a Base de Datos para otras ideas.
Si utilizas el Flight PHP Framework, puedes registrar la clase ActiveRecord como un servicio, pero en realidad no tienes que hacerlo.
Flight::register('user', 'User', [ $pdo_connection ]); // entonces puedes usarlo así en un controlador, una función, etc. Flight::user()->find(1);
runway es una herramienta CLI para Flight que tiene un comando personalizado para esta biblioteca.
# Uso php runway make:record database_table_name [class_name] # Ejemplo php runway make:record users
Esto creará una nueva clase en el directorio app/records/ como UserRecord.php con el siguiente contenido:
app/records/
UserRecord.php
<?php declare(strict_types=1); namespace app\records; /** * Clase ActiveRecord para la tabla de usuarios. * @link https://docs.flightphp.com/awesome-plugins/active-record * * @property int $id * @property string $username * @property string $email * @property string $password_hash * @property string $created_dt */ class UserRecord extends \flight\ActiveRecord { /** * @var array $relations Establece las relaciones para el modelo * https://docs.flightphp.com/awesome-plugins/active-record#relationships */ protected array $relations = [ // 'relation_name' => [ self::HAS_MANY, 'RelatedClass', 'foreign_key' ], ]; /** * Constructor * @param mixed $databaseConnection La conexión a la base de datos */ public function __construct($databaseConnection) { parent::__construct($databaseConnection, 'users'); } }
find($id = null) : boolean|ActiveRecord
Busca un registro y lo asigna al objeto actual. Si pasas un $id de algún tipo, realizará una búsqueda en la clave primaria con ese valor. Si no se pasa nada, solo encontrará el primer registro en la tabla.
$id
Además, puedes pasarle otros métodos auxiliares para consultar tu tabla.
// encuentra un registro con algunas condiciones previas $user->notNull('password')->orderBy('id DESC')->find(); // encuentra un registro por un id específico $id = 123; $user->find($id);
findAll(): array<int,ActiveRecord>
Encuentra todos los registros en la tabla que especifiques.
$user->findAll();
isHydrated(): boolean
Devuelve true si el registro actual ha sido hidratado (fetched de la base de datos).
true
$user->find(1); // si se encuentra un registro con datos... $user->isHydrated(); // true
insert(): boolean|ActiveRecord
Inserta el registro actual en la base de datos.
$user = new User($pdo_connection); $user->name = 'demo'; $user->password = md5('demo'); $user->insert();
Si tienes una clave primaria basada en texto (como un UUID), puedes establecer el valor de la clave primaria antes de insertar de una de dos maneras.
$user = new User($pdo_connection, [ 'primaryKey' => 'uuid' ]); $user->uuid = 'some-uuid'; $user->name = 'demo'; $user->password = md5('demo'); $user->insert(); // o $user->save();
o puedes hacer que la clave primaria se genere automáticamente para ti a través de eventos.
class User extends flight\ActiveRecord { public function __construct($database_connection) { parent::__construct($database_connection, 'users', [ 'primaryKey' => 'uuid' ]); // también puedes establecer el primaryKey así en lugar del array anterior. $this->primaryKey = 'uuid'; } protected function beforeInsert(self $self) { $self->uuid = uniqid(); // o como necesites generar tus id únicos } }
Si no estableces la clave primaria antes de insertar, se establecerá en el rowid y la base de datos la generará por ti, pero no persistirá porque ese campo puede no existir en tu tabla. Por eso se recomienda usar el evento para manejar esto automáticamente por ti.
rowid
update(): boolean|ActiveRecord
Actualiza el registro actual en la base de datos.
$user->greaterThan('id', 0)->orderBy('id desc')->find(); $user->email = 'test@example.com'; $user->update();
save(): boolean|ActiveRecord
Inserta o actualiza el registro actual en la base de datos. Si el registro tiene un id, se actualizará, de lo contrario, se insertará.
$user = new User($pdo_connection); $user->name = 'demo'; $user->password = md5('demo'); $user->save();
Nota: Si tienes relaciones definidas en la clase, también se guardarán recursivamente esas relaciones si se han definido, instanciado y tienen datos sucios que actualizar. (v0.4.0 y superiores)
delete(): boolean
Elimina el registro actual de la base de datos.
$user->gt('id', 0)->orderBy('id desc')->find(); $user->delete();
También puedes eliminar múltiples registros ejecutando primero una búsqueda.
$user->like('name', 'Bob%')->delete();
dirty(array $dirty = []): ActiveRecord
Los datos sucios se refieren a los datos que han cambiado en un registro.
$user->greaterThan('id', 0)->orderBy('id desc')->find(); // nada está "sucio" hasta este momento. $user->email = 'test@example.com'; // ahora el email se considera "sucio" ya que ha cambiado. $user->update(); // ahora no hay datos sucios porque se han actualizado y persistido en la base de datos $user->password = password_hash('newpassword'); // ahora esto está sucio $user->dirty(); // al no pasar nada, se limpiarán todas las entradas sucias. $user->update(); // no se actualizará nada porque no se capturó nada como sucio. $user->dirty([ 'name' => 'algo', 'password' => password_hash('una contraseña diferente') ]); $user->update(); // tanto nombre como contraseña se actualizan.
copyFrom(array $data): ActiveRecord
Este es un alias para el método dirty(). Es un poco más claro lo que estás haciendo.
dirty()
$user->copyFrom([ 'name' => 'algo', 'password' => password_hash('una contraseña diferente') ]); $user->update(); // tanto nombre como contraseña se actualizan.
isDirty(): boolean
Devuelve true si el registro actual ha cambiado.
$user->greaterThan('id', 0)->orderBy('id desc')->find(); $user->email = 'test@email.com'; $user->isDirty(); // true
reset(bool $include_query_data = true): ActiveRecord
Restablece el registro actual a su estado inicial. Esto es realmente bueno de usar en comportamientos de tipo bucle. Si pasas true, también restablecerá los datos de consulta que se usaron para encontrar el objeto actual (comportamiento predeterminado).
$users = $user->greaterThan('id', 0)->orderBy('id desc')->find(); $user_company = new UserCompany($pdo_connection); foreach($users as $user) { $user_company->reset(); // comienza con una pizarra limpia $user_company->user_id = $user->id; $user_company->company_id = $some_company_id; $user_company->insert(); }
getBuiltSql(): string
Después de ejecutar un método find(), findAll(), insert(), update(), o save() puedes obtener el SQL que fue construido y usarlo para fines de depuración.
find()
findAll()
insert()
update()
save()
select(string $field1 [, string $field2 ... ])
Puedes seleccionar solo algunas de las columnas en una tabla si lo deseas (es más eficiente en tablas realmente anchas con muchas columnas)
$user->select('id', 'name')->find();
from(string $table)
¡Técnicamente puedes elegir otra tabla también! ¿Por qué no?!
$user->select('id', 'name')->from('user')->find();
join(string $table_name, string $join_condition)
Incluso puedes unirte a otra tabla en la base de datos.
$user->join('contacts', 'contacts.user_id = users.id')->find();
where(string $where_conditions)
Puedes establecer algunos argumentos where personalizados (no puedes establecer parámetros en esta declaración where)
$user->where('id=1 AND name="demo"')->find();
Nota de Seguridad - Podrías estar tentado a hacer algo como $user->where("id = '{$id}' AND name = '{$name}'")->find();. ¡Por favor, NO HAGAS ESTO! Esto es susceptible a lo que se conoce como ataques de inyección SQL. Hay muchos artículos en línea, por favor, Google "ataques de inyección sql php" y encontrarás muchos artículos sobre este tema. La forma adecuada de manejar esto con esta biblioteca es en lugar de este método where(), harías algo más como $user->eq('id', $id)->eq('name', $name)->find(); Si absolutamente tienes que hacer esto, la biblioteca PDO tiene $pdo->quote($var) para escapar por ti. Solo después de usar quote() puedes usarlo en una declaración where().
$user->where("id = '{$id}' AND name = '{$name}'")->find();
where()
$user->eq('id', $id)->eq('name', $name)->find();
PDO
$pdo->quote($var)
quote()
group(string $group_by_statement)/groupBy(string $group_by_statement)
Agrupa tus resultados por una condición particular.
$user->select('COUNT(*) as count')->groupBy('name')->findAll();
order(string $order_by_statement)/orderBy(string $order_by_statement)
Ordena la consulta devuelta de una manera determinada.
$user->orderBy('name DESC')->find();
limit(string $limit)/limit(int $offset, int $limit)
Limita la cantidad de registros devueltos. Si se da un segundo entero, será desplazado, limite igual que en SQL.
$user->orderby('name DESC')->limit(0, 10)->findAll();
equal(string $field, mixed $value) / eq(string $field, mixed $value)
Donde field = $value
field = $value
$user->eq('id', 1)->find();
notEqual(string $field, mixed $value) / ne(string $field, mixed $value)
Donde field <> $value
field <> $value
$user->ne('id', 1)->find();
isNull(string $field)
Donde field IS NULL
field IS NULL
$user->isNull('id')->find();
isNotNull(string $field) / notNull(string $field)
Donde field IS NOT NULL
field IS NOT NULL
$user->isNotNull('id')->find();
greaterThan(string $field, mixed $value) / gt(string $field, mixed $value)
Donde field > $value
field > $value
$user->gt('id', 1)->find();
lessThan(string $field, mixed $value) / lt(string $field, mixed $value)
Donde field < $value
field < $value
$user->lt('id', 1)->find();
greaterThanOrEqual(string $field, mixed $value) / ge(string $field, mixed $value) / gte(string $field, mixed $value)
Donde field >= $value
field >= $value
$user->ge('id', 1)->find();
lessThanOrEqual(string $field, mixed $value) / le(string $field, mixed $value) / lte(string $field, mixed $value)
Donde field <= $value
field <= $value
$user->le('id', 1)->find();
like(string $field, mixed $value) / notLike(string $field, mixed $value)
Donde field LIKE $value o field NOT LIKE $value
field LIKE $value
field NOT LIKE $value
$user->like('name', 'de')->find();
in(string $field, array $values) / notIn(string $field, array $values)
Donde field IN($value) o field NOT IN($value)
field IN($value)
field NOT IN($value)
$user->in('id', [1, 2])->find();
between(string $field, array $values)
Donde field BETWEEN $value AND $value1
field BETWEEN $value AND $value1
$user->between('id', [1, 2])->find();
Es posible envolver tus condiciones en una declaración OR. Esto se hace con los métodos startWrap() y endWrap() o llenando el tercer parámetro de la condición después del campo y el valor.
startWrap()
endWrap()
// Método 1 $user->eq('id', 1)->startWrap()->eq('name', 'demo')->or()->eq('name', 'test')->endWrap('OR')->find(); // Esto evaluará a `id = 1 AND (name = 'demo' OR name = 'test')` // Método 2 $user->eq('id', 1)->eq('name', 'demo', 'OR')->find(); // Esto evaluará a `id = 1 OR name = 'demo'`
Puedes establecer varios tipos de relaciones utilizando esta biblioteca. Puedes establecer relaciones uno->muchos y uno->uno entre tablas. Esto requiere una configuración adicional en la clase por adelantado.
Establecer el array $relations no es difícil, pero adivinar la sintaxis correcta puede ser confuso.
$relations
protected array $relations = [ // puedes nombrar la clave como desees. El nombre de ActiveRecord probablemente sea bueno. Ej: user, contact, client 'user' => [ // requerido // self::HAS_MANY, self::HAS_ONE, self::BELONGS_TO self::HAS_ONE, // este es el tipo de relación // requerido 'Some_Class', // esta es la clase ActiveRecord "otra" a la que se referirá // requerido // dependiendo del tipo de relación // self::HAS_ONE = la clave foránea que referencia la unión // self::HAS_MANY = la clave foránea que referencia la unión // self::BELONGS_TO = la clave local que referencia la unión 'local_or_foreign_key', // solo FYI, esto también solo se une a la clave primaria del "otro" modelo // opcional [ 'eq' => [ 'client_id', 5 ], 'select' => 'COUNT(*) as count', 'limit' 5 ], // condiciones adicionales que deseas al unir la relación // $record->eq('client_id', 5)->select('COUNT(*) as count')->limit(5)) // opcional 'back_reference_name' // esto es si deseas referenciar esta relación de regreso hacia sí misma Ej: $user->contact->user; ]; ]
class User extends ActiveRecord{ protected array $relations = [ 'contacts' => [ self::HAS_MANY, Contact::class, 'user_id' ], 'contact' => [ self::HAS_ONE, Contact::class, 'user_id' ], ]; public function __construct($database_connection) { parent::__construct($database_connection, 'users'); } } class Contact extends ActiveRecord{ protected array $relations = [ 'user' => [ self::BELONGS_TO, User::class, 'user_id' ], 'user_with_backref' => [ self::BELONGS_TO, User::class, 'user_id', [], 'contact' ], ]; public function __construct($database_connection) { parent::__construct($database_connection, 'contacts'); } }
¡Ahora hemos configurado las referencias para poder utilizarlas muy fácilmente!
$user = new User($pdo_connection); // encuentra el usuario más reciente. $user->notNull('id')->orderBy('id desc')->find(); // obtén contactos usando relación: foreach($user->contacts as $contact) { echo $contact->id; } // o podemos hacerlo al revés. $contact = new Contact(); // encuentra un contacto $contact->find(); // obtén usuario usando relación: echo $contact->user->name; // este es el nombre del usuario
¿Bastante genial, eh?
A veces puede que necesites adjuntar algo único a tu ActiveRecord, como un cálculo personalizado que podría ser más fácil de adjuntar al objeto que luego se pasará, digamos, a una plantilla.
setCustomData(string $field, mixed $value)
Adjuntas los datos personalizados con el método setCustomData().
setCustomData()
$user->setCustomData('page_view_count', $page_view_count);
Y luego simplemente lo referencias como una propiedad normal del objeto.
echo $user->page_view_count;
Una característica más increíble de esta biblioteca es sobre los eventos. Los eventos se activan en ciertos momentos basados en ciertos métodos que llamas. Son muy útiles para configurar datos automáticamente para ti.
onConstruct(ActiveRecord $ActiveRecord, array &config)
Esto es realmente útil si necesitas establecer una conexión predeterminada o algo así.
// index.php o bootstrap.php Flight::register('db', 'PDO', [ 'sqlite:test.db' ]); // // // // User.php class User extends flight\ActiveRecord { protected function onConstruct(self $self, array &$config) { // no olvides la referencia & // podrías hacer esto para establecer automáticamente la conexión $config['connection'] = Flight::db(); // o esto $self->transformAndPersistConnection(Flight::db()); // También puedes establecer el nombre de la tabla de esta manera. $config['table'] = 'users'; } }
beforeFind(ActiveRecord $ActiveRecord)
Esto es probablemente útil solo si necesitas una manipulación de consulta cada vez.
class User extends flight\ActiveRecord { public function __construct($database_connection) { parent::__construct($database_connection, 'users'); } protected function beforeFind(self $self) { // siempre ejecutar id >= 0 si eso es lo que prefieres $self->gte('id', 0); } }
afterFind(ActiveRecord $ActiveRecord)
Este probablemente sea más útil si siempre necesitas ejecutar alguna lógica cada vez que se recupera este registro. ¿Necesitas descifrar algo? ¿Necesitas ejecutar una consulta de conteo personalizada cada vez (no es eficiente, pero bueno)?
class User extends flight\ActiveRecord { public function __construct($database_connection) { parent::__construct($database_connection, 'users'); } protected function afterFind(self $self) { // descifrando algo $self->secret = yourDecryptFunction($self->secret, $some_key); // tal vez almacenar algo personalizado como una consulta??? $self->setCustomData('view_count', $self->select('COUNT(*) count')->from('user_views')->eq('user_id', $self->id)['count']); } }
beforeFindAll(ActiveRecord $ActiveRecord)
class User extends flight\ActiveRecord { public function __construct($database_connection) { parent::__construct($database_connection, 'users'); } protected function beforeFindAll(self $self) { // siempre ejecutar id >= 0 si eso es lo que prefieres $self->gte('id', 0); } }
afterFindAll(array<int,ActiveRecord> $results)
Similar a afterFind(), pero ¡puedes hacerlo a todos los registros!
afterFind()
class User extends flight\ActiveRecord { public function __construct($database_connection) { parent::__construct($database_connection, 'users'); } protected function afterFindAll(array $results) { foreach($results as $self) { // haz algo genial como afterFind() } } }
beforeInsert(ActiveRecord $ActiveRecord)
Realmente útil si necesitas establecer algunos valores predeterminados cada vez.
class User extends flight\ActiveRecord { public function __construct($database_connection) { parent::__construct($database_connection, 'users'); } protected function beforeInsert(self $self) { // establecer algunos valores predeterminados sensatos if(!$self->created_date) { $self->created_date = gmdate('Y-m-d'); } if(!$self->password) { $self->password = password_hash((string) microtime(true)); } } }
afterInsert(ActiveRecord $ActiveRecord)
Tal vez tengas un caso de uso para cambiar datos después de que se inserte.
class User extends flight\ActiveRecord { public function __construct($database_connection) { parent::__construct($database_connection, 'users'); } protected function afterInsert(self $self) { // tú haz lo que quieras Flight::cache()->set('most_recent_insert_id', $self->id); // o lo que sea.... } }
beforeUpdate(ActiveRecord $ActiveRecord)
Realmente útil si necesitas establecer algunos valores predeterminados cada vez que se actualiza.
class User extends flight\ActiveRecord { public function __construct($database_connection) { parent::__construct($database_connection, 'users'); } protected function beforeUpdate(self $self) { // establece algunos valores predeterminados sensatos if(!$self->updated_date) { $self->updated_date = gmdate('Y-m-d'); } } }
afterUpdate(ActiveRecord $ActiveRecord)
Tal vez tengas un caso de uso para cambiar datos después de que se actualice.
class User extends flight\ActiveRecord { public function __construct($database_connection) { parent::__construct($database_connection, 'users'); } protected function afterUpdate(self $self) { // tú haz lo que quieras Flight::cache()->set('most_recently_updated_user_id', $self->id); // o lo que sea.... } }
beforeSave(ActiveRecord $ActiveRecord)/afterSave(ActiveRecord $ActiveRecord)
Esto es útil si deseas que los eventos ocurran tanto cuando se realicen inserciones como actualizaciones. Te ahorraré la larga explicación, pero estoy seguro de que puedes adivinar lo que es.
class User extends flight\ActiveRecord { public function __construct($database_connection) { parent::__construct($database_connection, 'users'); } protected function beforeSave(self $self) { $self->last_updated = gmdate('Y-m-d H:i:s'); } }
beforeDelete(ActiveRecord $ActiveRecord)/afterDelete(ActiveRecord $ActiveRecord)
No estoy seguro de qué querrías hacer aquí, ¡pero no hay juicios! ¡Ve por ello!
class User extends flight\ActiveRecord { public function __construct($database_connection) { parent::__construct($database_connection, 'users'); } protected function beforeDelete(self $self) { echo 'Él fue un valiente soldado... :cry-face:'; } }
Cuando utilizas esta biblioteca, puedes establecer la conexión de base de datos de varias maneras. Puedes establecer la conexión en el constructor, puedes establecerla a través de una variable de configuración $config['connection'] o puedes establecerla a través de setDatabaseConnection() (v0.4.1).
$config['connection']
setDatabaseConnection()
$pdo_connection = new PDO('sqlite:test.db'); // por ejemplo $user = new User($pdo_connection); // o $user = new User(null, [ 'connection' => $pdo_connection ]); // o $user = new User(); $user->setDatabaseConnection($pdo_connection);
Si deseas evitar establecer siempre un $database_connection cada vez que llamas a un registro activo, ¡hay formas de evitarlo!
$database_connection
// index.php o bootstrap.php // Establece esto como una clase registrada en Flight Flight::register('db', 'PDO', [ 'sqlite:test.db' ]); // User.php class User extends flight\ActiveRecord { public function __construct(array $config = []) { $database_connection = $config['connection'] ?? Flight::db(); parent::__construct($database_connection, 'users', $config); } } // ¡Y ahora, no se requieren argumentos! $user = new User();
Nota: Si planeas realizar pruebas unitarias, hacerlo de esta manera puede agregar algunos desafíos a las pruebas unitarias, pero en general, porque puedes inyectar tu conexión con setDatabaseConnection() o $config['connection'] no es tan malo.
Si necesitas refrescar la conexión a la base de datos, por ejemplo, si estás ejecutando un script CLI de larga duración y necesitas refrescar la conexión de vez en cuando, puedes restablecer la conexión con $your_record->setDatabaseConnection($pdo_connection).
$your_record->setDatabaseConnection($pdo_connection)
Por favor hazlo. :D
Cuando contribuyas, asegúrate de ejecutar composer test-coverage para mantener una cobertura de prueba del 100% (esto no es una cobertura de prueba unitarias verdadera, más bien una prueba de integración).
composer test-coverage
También asegura ejecutar composer beautify y composer phpcs para corregir cualquier error de formato.
composer beautify
composer phpcs
MIT