Patrón ActiveTable y ActiveRecord
Cuando empezamos a utilizar OOP al 100% empezamos a usar clases para representar objetos que estamos utilizando en el diseño de nuestro sistema.
Pensemos en un ejemplo básico, vamos a hacer un catalogo de automóviles, para representar a nuestro automóvil vamos a utilizar la siguiente clase:
1 <?php
2 class Automovil {
3 private $nombre;
4 private $color;
5 private $puertas;
6 private $maxVelocidad;
7
8 public function __construct() {}
9
10 /** Setters **/
11 public function setNombre( $nombre ) {
12 $this->nombre = $nombre;
13 }
14
15 public function setColor( $color ) {
16 $this->color = $color;
17 }
18
19 public function setPuertas( $puertas ) {
20 $this->puertas = $puertas;
21 }
22
23 public function setMaxVelocidad( $velocidad ) {
24 $this->maxVelocidad = $velocidad;
25 }
26
27 /** Getters **/
28 public function getNombre() {
29 return $this->nombre;
30 }
31
32 public function getColor() {
33 return $this->color;
34 }
35
36 public function getPuertas() {
37 return $this->puertas;
38 }
39
40 public function getMaxVelocidad() {
41 return $this->maxVelocidad;
42 }
43 }
44 ?>
Como podemos ver nuestro automóvil tiene propiedades básicas, puede ser más complejo pero para este tutorial lo dejaremos así. Podemos utilizar nuestro nuevo objeto Automóvil de la siguiente manera:
1 <?php
2 $auto = new Automovil();
3 $auto->setNombre("BMW M3");
4 $auto->setColor("Negro");
5 $auto->setPuertas( 4 );
6 $auto->setMaxVelocidad( "250 km/h" );
7 ?>
Es muy sencillo, pero se complica cuando queremos que este automóvil sea persistente, o sea que este guardado en un medio no volátil, como una base de datos, XML, archivo de texto, etc. Esto es fácil de implementar, por ejemplo usando serialize y guardándolo en esos medios, pero ¿cómo lo recuperamos?, ¿Cómo podemos hacer búsqueda por cierta característica (ej: el color)? Aquí es donde se complica todo, ya que si queremos tener una tabla en nuestra base de datos donde cada propiedad de nuestra clase represente una columna de nuestra base de datos, y cada instancia de la clase represente una fila de nuestra tabla necesitamos de cierta forma hacer que se traduzca la clase en un objeto que se pueda insertar en nuestra base de datos.
La forma de hacer esto: ActiveRecord, este es un patrón de diseño, con el cual podemos implementar de manera correcta que cada objeto nos represente una fila de nuestra base de datos.
¿Qué es el ActiveTable?
El Active Table es un objeto que puede persistir otros objetos en medios no volátiles, como bases de datos. La función de una clase que implementa este patrón es de comunicarse con la base de datos y regresarnos objetos como los tenemos definidos para nuestra aplicación. Veamos el siguiente ejemplo:
1 <?php
2 $autos = new Automoviles();
3 $unAuto = $autos->findByPk(1); // Recuperamos el Auto con ID=1
4 echo $unAuto['Nombre'];
5 $unAuto['Puertas'] = 3; // Cambiamos el numero de puertas
6 $unAuto->save(); // Guardamos la modificacion.
7 ?>
Como podemos ver, nuestro ActiveTable Automóviles nos trae un objeto automóvil, sobre el cual podemos trabajar e incluso guardar directamente en la base de datos, el objeto es lo suficientemente inteligente para saber guardarse o crear uno nuevo.
¿Cómo implementamos este patrón?
Hay varias formas de implementarlo, muchos Frameworks lo hacen a su manera, yo en lo personal me gusta la siguiente forma, en la que heredando la clase directamente, podemos saber el nombre de la tabla y usamos comandos simples para obtener los datos que nos interesan (como la clave primaria).
Una vez creado nuestro objeto, a la hora de pedirle que nos regrese algo (con alguna de las funciones find*), la clase debe de crear y regresarnos un objeto que podamos utilizar (en nuestro caso un objeto Automóvil). Este objeto nos sirve para trabajar como lo vimos en el primer ejemplo, pero también a la hora de implementar ActiveRecord es posible que el objeto se sepa guardar en la base de datos solos.
Veamos el siguiente ejemplo de ActiveTable:
1 <?php
2 class ActiveTable {
3 protected $table;
4 protected $keyField;
5 protected $_db;
6 protected $resultObject;
7 private $_columns;
8
9 public function __construct() {
10 $this->_db = DB::getInstance();
11 $this->setup();
12 }
13
14 public function __destruct() {}
15
16 protected function setup() {
17 if( !$this->table ) $this->table = get_class($this);
18
19 $query = "SHOW FIELDS FROM ?";
20 $command = $this->_db->prepare( $query );
21 $command->execute( array( $this->table ) );
22
23 $fields = array();
24 $primary = '';
25 while( $row = $command->fetch() ) {
26 $fields[$field['Field']] = array(
27 "name" => $field['Field'],
28 "type" => $field['Type'],
29 "defaultValue" => $field['Default'],
30 "key" => $field['Key'],
31 );
32
33 if( $field['Key'] === "PRI" ) {
34 $primary = $field['Field'];
35 }
36 }
37
38 $this->_columns = $fields;
39
40 if( empty( $primary ) ) {
41 throw new Exception( "Primary Column not found for Table: " . $this->table );
42 }
43
44 $this->keyField = $primary;
45 }
46
47 public function info() {
48 return array(
49 "name" => $this->table,
50 "columns" => $this->_columns,
51 "primary" => $this->keyField,
52 );
53 }
54
55 public function getPrimaryKey() {
56 return $this->keyField;
57 }
58
59 public function newRecord() {
60 $obj = $this->resultObject;
61
62 $aObj = new $obj(null, $this);
63 return $aObj;
64 }
65
66 public function findByPk( $id ) {
67 $sql = "SELECT * FROM `%s` WHERE `%s`='%s' LIMIT 1";
68 $sql = sprintf($sql, $this->table, $this->keyField, $id);
69
70 $result = $this->_db->query( $sql );
71 $data = $result->fetch();
72
73 $obj = $this->resultObject;
74
75 $aObj = new $obj($data, $this);
76
77 return $aObj;
78 }
79
80 public function add( $fields, $function_fields = array() ) {
81 $qvalues = array();
82 $qfields = array();
83 foreach( $fields as $field_name => $field_value ) {
84 $qfields[] = "`$field_name`";
85 if( !empty( $function_fields[$field_name] ) ) {
86 $qvalues[] = $function_fields[$field_name] . "('$field_value')";
87 } else {
88 $qvalues[] = "'" . addslashes( $field_value ) . "'";
89 }
90 }
91
92 $query = "INSERT INTO `" . $this->table . "` (" . implode( ', ', $qfields ) . ") VALUES ( " . implode( ', ', $qvalues ) . ")";
93
94 $affRows = $this->_db->exec( $query );
95 $id = $this->_db->lastInsertId();
96
97 if( $affRows <= 0 ) {
98 throw new Exception( "SQL Error, Insert Failed" );
99 }
100
101 return $id;
102 }
103
104 public function save( $fields, $strict = true, $function_fields = array() ) {
105 if( ( isset( $fields[$this->keyField] ) ) && ( !empty( $fields[$this->keyField] ) ) && $this->keyExists( $fields[$this->keyField] ) ) {
106 $result = $this->update( $fields, $strict, $function_fields );
107 } else {
108 $result = $this->add( $fields, $function_fields );
109 }
110
111 return $result;
112 }
113
114 public function update( $fields, $strict = true, $function_fields = array() ) {
115 if( !isset( $fields[$this->keyField] ) || empty( $fields[$this->keyField] ) ) {
116 throw new Exception( "Primary Key Missing, update failed" );
117 }
118
119 $key = $fields[$this->keyField];
120 unset( $fields[$this->keyField] );
121
122 if( $strict ) {
123 $rst = $this->_db->query( sprintf( "SELECT * FROM `%s` WHERE `%s`='%s' LIMIT 1", $this->table, $this->keyField, $key ) );
124 $data = $rst->fetch();
125 } else {
126 $data = array();
127 }
128
129 $toupdate = array();
130 foreach( $fields as $field=>$value ) {
131 if( $strict ) {
132 if( ( $data[$field] != $value ) ) {
133 if( !empty( $function_fields[$field] ) ) {
134 $toupdate[] = sprintf( "`%s`=%s('%s')", $field, $function_fields[$field], $value );
135 } else {
136 $toupdate[] = sprintf( "`%s`='%s'", $field, addslashes( $value ) );
137 }
138 }
139 } else {
140 if( !empty( $function_fields[$field] ) ) {
141 $toupdate[] = sprintf( "`%s`=%s('%s')", $field, $function_fields[$field], $value );
142 } else {
143 $toupdate[] = sprintf( "`%s`='%s'", $field, addslashes( $value ) );
144 }
145 }
146 }
147 if( count( $toupdate ) == 0 ) {
148 return 0;
149 }
150
151 $query = sprintf( "UPDATE `%s` SET %s WHERE `%s`='%s' LIMIT 1", $this->table, implode( ', ', $toupdate ), $this->keyField, $key );
152
153 $updated = $this->_db->exec( $query );
154
155 if( $updated <= 0 ) { // if true, then some wierd database error ocurred
156 return 0;
157 }
158
159 return $updated;
160 }
161
162 public function delete( $records ) {
163 if( empty( $records ) ) return 0;
164
165 if( is_array( $records ) ) {
166 $escaped_keys = array();
167
168 foreach( $records as $key ) {
169 $escaped_keys[] = "'{$key}'";
170 }
171 $keys = implode(",", $escaped_keys);
172 $total = count( $escaped_keys );
173
174 $query = sprintf( "DELETE FROM `%s` WHERE `%s` IN (%s) LIMIT %s", $this->table, $this->keyField, $keys, $total );
175 } else {
176 $total = 1;
177 $query = sprintf( "DELETE FROM `%s` WHERE `%s`='%s' LIMIT %s", $this->table, $this->keyField, $records, $total );
178 }
179
180 $deleted = $this->_db->exec( $query );
181
182 return $deleted;
183 }
184 }
185 ?>
Este es un ejemplo sencillo desde el cual ustedes pueden seguir y completarlo e implementar ya cosas más complejas.
¿Qué es el ActiveRecord?
El Active Record es un patrón de diseño donde a la hora de implementarlo debe de ser inteligente para saber guardarse en la base de datos con solo llamar a su función save.
¿Cómo implementamos este patrón?
Una forma sencilla y rápida es usando ArrayAccess y heredando directamente de ActiveRecord, una implementación sencilla pero efectiva puede ser la siguiente:
1 <?php
2 class ActiveRecord implements ArrayAccess {
3 private $record = array();
4 private $keyField = '';
5 private $table = null;
6 private $isNew = false;
7
8 public function __construct( $data, $table ) {
9 if( $data === null ) {
10 $this->record = array();
11 $this->isNew = true;
12 } else {
13 $this->record = $data;
14 }
15
16 if( !( $table instanceof ActiveTable ) ) {
17 throw new Exception( '$table is expected to be a GeckoDBTable instance' );
18 }
19
20 $this->table = $table;
21 $this->keyField = $table->getPrimaryKey();
22 }
23
24 public function refresh() {
25 if( $this->isNew ) return; // new Records can't be refreshed
26
27 $newData = $this->table->findByPk( $this->record[$this->keyField] );
28 $this->record = $newData->record;
29 $newData = null;
30 }
31
32 public function save() {
33 if( $this->isNew ) {
34 $id = $this->table->add( $this->record );
35 $this->record[$this->keyField] = $id;
36 $this->isNew = false;
37
38 return $id;
39 } else {
40 return $this->table->update( $this->record );
41 }
42 }
43
44 public function getNew() {
45 return new self( null, $this->table );
46 }
47
48 public function offsetExists( $offset ) {
49 return (isset( $this->record[$offset] ) );
50 }
51
52 public function offsetGet( $offset ) {
53 return $this->record[$offset];
54 }
55
56 public function offsetSet( $offset, $value ) {
57 if( $offset == $this->keyField ) {
58 throw new Exception( "Primary Key can't be set or changed" );
59 }
60
61 $this->record[$offset] = $value;
62 }
63
64 public function offsetUnset( $offset ) {
65 if( isset( $this->record[$offset ) ) {
66 unset( $this->record[$offset] );
67 }
68 }
69 }
70 ?>
Ahora ya teniendo nuestros patrones y nuestras clases listas, ¿cómo lo utilizamos?, veamos el siguiente ejemplo completo:
1 <?php
2 include( 'ActiveTable.php' );
3 include( 'ActiveRecord.php' );
4
5 class Automoviles extends ActiveTable {
6 public function __construct() {
7 parent::__construct();
8
9 $this->resultObject = "Automovil";
10 }
11 }
12
13 class Automovil extends ActiveRecord {}
14
15 $autos = new Automoviles();
16 $unAuto = $autos->findByPk(1); // Recuperamos el Auto con ID=1
17 echo $unAuto['Nombre'];
18 $unAuto['Puertas'] = 3; // Cambiamos el numero de puertas
19 $unAuto->save(); // Guardamos la modificacion.
20 ?>
Como podemos ver, no usamos 1 sola sentencia SQL directamente, esto nos permite tener un nivel de abstracción muchísimo mayor e inclusive a la hora de programar es mucho más rápido.
De aquí faltan mucho más cosas, como por ejemplo hace unos meses en ForosDelWeb alguien decía que esto no era efectivo porque cuando tenemos claves foráneas, teníamos que crear dos instancias y las clases hacían múltiples consultas a la base de datos, esto es cierto, pero no totalmente, podemos modificar nuestra clase para que sea lo suficientemente inteligente y pueda resolver las claves foráneas, y crear Joins, esto es más complicado y no se va a cubrir en este tutorial, pero es posible al 100%, y de manera eficiente.
Espero que con este tutorial les haya quedado como implementar estos patrones de diseño y puedan realizar sus aplicaciones más limpias.
Aqui les dejo un link para que vean como implementar ArrayAccess y como nos permite accesar a las propiedades de un objeto como si fuera un Arreglo.

17 comentarios:
hola, esta muy bueno el articulo, también se me ocurre usar los métodos mágicos __get y __set para modificar o obtener los valores del registro en cuestión.
52 public function __get( $offset ) {
53 return $this->record[$offset];
54 }
55
56 public function __set( $offset, $value ) {
57 if( $offset == $this->keyField ) {
58 throw new Exception( "Primary Key can't be set or changed" );
59 }
60
61 $this->record[$offset] = $value;
62 }
felicitaciones y saludos.
Si, se puede hacer asi, aunque a mi gusto es mas limpio si se usa como un array, claro es algo personal ;-)
Gracias por leer mi Blog.
Excelente tu blog, gracias a ti por compartir tus conocimientos y felicitaciones.
Saludos.
hola, exelente en verdad el articulo te lo agradesco ...
me ha ayudado a enteder un poquito mas la poo, creo que ya es hora de empezar a utilizar cosas como estas...
disculpa mi ignorancia pero tengo una duda: el uso de `%s` ?? por el contexto en las sentecias sql deben ser los valores que le pasas.. bueno yo lo habia usado con este (?).
y me gustaria hacerte una pequena recomendacion. si puedes en ocasiones usaras algunos comentarios mas en el codigo( solo los necesarios)... creeme que nos ayudarias a enteder un poquito mejor el codigo
Un saludo
Gracias Chris, tenía muchísimas ganas de leer este artículo.
Muchas felicidades ! ;-)
Hola, espero que poco a poco vayas poniendo artículos como este.
De momento, aunque haya poco contenido, aquí hay cosas que me resultan interesantes.
Yo estoy intentando hacer mi propio framework y acabo de descubrir que existe esto de ActiveRecord y ActiveTable que yo estaba intentando implementar a mi manera y con otro nombre (porque ignoraba que existía).
Respecto a esto tengo una duda, ¿cómo harías para recuperar un conjunto de registros en vez de uno? Supongamos que tenemos un método findByFk() en la classe ActiveTable y que funciona correctamente...
¿Harías esto?:
$autos = new Automoviles();
$variosAutos = $autos->findByFk(...);
¿O implementarías alguna classe como ActiveRecord que te ayude a manipular mejor un conjunto de registros?
Un saludo.
masterjail:
En tu clase ActiveTable implementas un metodo que se llame ej:
findAll();
findAllByColor();
etc,
Es lo importante de la POO, que puedes heredar a tu clase principal y asi implementar funciones especificas, cada una de estas funciones si te regresan varios registros, te deben de regresar un conjunto de objetos tipo Automovil en el mejor de los casos.
Saludos.
Hola, podrias explicar brevemente estas lineas??
$obj = $this->resultObject;
$aObj = new $obj($data, $this);
que valor tiene $this->resultObject ?????
Saludos
perdon, no habia visto la ultima parte
Saludos
Antes que nada felicidades Christopher por el artículo. La verdad es que es una introducción realmente útil al tema de los patrones de PHP :-D
Tenía la siguiente duda: Estoy provando con la clase ActiveTable. La he copiado directamente y ahora estoy viendo su funcionamiento paso a paso. Sin embargo me he encontrado con un problema. En la línea 21 que escribe "$command->execute( array( $this->table ) );" me da el siguiente error: Fatal error: Call to a member function execute() on a non-object. Si en la línea 19 ($query = "SHOW FIELDS FROM ?";) cambio el "?" por "Automoviles" ya no me da el error.
Mirando sobre el tema he leído la siguiente nota en php.net: PDO::prepare Problem la cual me da a entender que el script no puede funcionar. Cómo habeis conseguido que os funcione vosotros?
Quizá tiene algo que ver la versión PHP? Yo tengo la 5.1.6
Que tal javier_sexias, te recomendaria que actualizaras a la version mas reciente para PHP, y la razon por la que uso placeholders (?) Es porque es mas sencillo para PDO emular los prepared statements para drivers que no los soportan nativamente.
Saludos.
loading..........
Muy buen articulo, pero quita los números de los ejemplos. No es fácil hace un copy & paste.
Maborak.
connection closed.
excelente articulo. seria bueno q hagas un framework con ActiveRecord y ActiveTable como kumbia framework mucha gente estaria dispuesta a ayudarte.
Saludos, te cuento llege a tu blog enseñandole a un amigo un poco sobre mvc y me ha gustado es un blog bastante interesate.. espero sigas publicand buen contenido
Saludos
Excelente blog y muy util por cierto, gracias por compartir estos conocimientos que son de mucha utilidad.
consulta:
bueno quisiera que me ayudaras con esto:
Al llamarse la funcion setup() desde el constructor de ActiveTable
se muestra el siguiente error:
Primary Column not found for Tablecreo que se debe a que no encuentra la PrimaryKey de la tabla, pero la tabla a la que se hace referencia en la cunsulta si cuenta con clave primaria.
quisiera saver porq no me retorna la clave.
Saludos!!
y Gracias de antemano!!!
Hola, muy bueno el post. Si no es mucho pedir, Podrías agregar la clase db?
Muchas gracias
Hola, muy bueno el post. Si no es mucho pedir, Podrías agregar la clase db?
Muchas gracias
Publicar un comentario en la entrada