| Viewing file:  toba_datos_tabla.php (61.79 KB)      -rw-r--r-- Select action/file-type:
 
  (+) |  (+) |  (+) | Code (+) | Session (+) |  (+) | SDB (+) |  (+) |  (+) |  (+) |  (+) |  (+) | 
 
<?php/**
 * Representa una estructura tabular tipo tabla o RecordSet en memoria
 *
 * - Utiliza un administrador de persistencia para obtener y sincronizar los datos con un medio de persistencia.
 * - Una vez en memoria existen primitivas para trabajar sobre estos datos.
 * - Los datos y sus modificaciones son mantenidos automáticamente en sesión entre los distintos pedidos de página.
 * - Una vez terminada la edición se hace la sincronización con el medio de persistencia marcando el final de la transacción de negocios.
 *
 * @package Componentes
 * @subpackage Persistencia
 * @todo Control de FK y PK
 */
 class toba_datos_tabla extends toba_componente
 {
 protected $_info_estructura;
 protected $_info_columnas;
 protected $_info_externas;
 protected $_info_externas_col;
 protected $_persistidor;                        // Mantiene el persistidor del OBJETO
 // Definicion asociada a la TABLA
 protected $_clave = array();                            // Columnas que constituyen la clave de la tabla
 protected $_columnas;
 protected $_posee_columnas_ext = false;        // Indica si la tabla posee columnas externas (cargadas a travez de un mecanismo especial)
 //Constraints
 protected $_no_duplicado;                    // Combinaciones de columnas que no pueden duplicarse
 // Definicion general
 protected $_tope_max_filas;                    // Cantidad de maxima de datos permitida.
 protected $_tope_min_filas;                    // Cantidad de minima de datos permitida.
 protected $_es_unico_registro=true;        //La tabla tiene com maximo un registro?
 // ESTADO
 protected $_cambios = array();                // Cambios realizados sobre los datos
 protected $_datos = array();                    // Datos cargados en el db_filas
 protected $_proxima_fila = 0;                // Posicion del proximo registro en el array de datos
 protected $_cursor;                            // Puntero a una fila específica
 protected $_cursor_original;                    // Backup del cursor que se usa para deshacer un seteo
 protected $_cargada = false;
 protected $_from;
 protected $_where;
 //Valores de las columnas BLOB
 protected $_blobs = array();                    //Arreglo [$fila][$columna]['fp' => fp, 'path' =>string, 'modificado' => boolean]
 // Si [$fila][$columna]
 //es null quiere decir que no esta cargado en la transaccion
 //es array(...,'modificado' => true) hay que actualizarlo a la base
 //es array(...,'modificado' => false) se cargo en la transacion pero no se modifico
 // Relaciones con el exterior
 protected $_relaciones_con_padres = array();            // ARRAY con un objeto RELACION por cada PADRE de la tabla
 protected $_relaciones_con_hijos = array();            // ARRAY con un objeto RELACION por cada HIJO de la tabla
 
 /**
 * @ignore
 */
 final function __construct($id)
 {
 $propiedades = array();
 $propiedades[] = "_cambios";
 $propiedades[] = "_datos";
 $propiedades[] = "_proxima_fila";
 $propiedades[] = "_cursor";
 $propiedades[] = "_cargada";
 $propiedades[] = "_blobs";
 $this->set_propiedades_sesion($propiedades);
 parent::__construct($id);
 for($a=0; $a < count($this->_info_columnas); $a++){
 //Armo una propiedad "columnas" para acceder a la definicion mas facil
 $this->_columnas[ $this->_info_columnas[$a]['columna'] ] =& $this->_info_columnas[$a];
 if($this->_info_columnas[$a]['pk']==1){
 $this->_clave[] = $this->_info_columnas[$a]['columna'];
 }
 if($this->_info_columnas[$a]['externa']==1){
 $this->_posee_columnas_ext = true;
 }
 }
 $this->activar_cargas_externas();
 $this->activar_control_valores_unicos();
 }
 
 
 /**
 * Ventana para agregar configuraciones particulares antes de que el objeto sea construido en su totalidad
 * @ventana
 */
 function ini(){}
 
 
 /**
 * Destructor del componente
 */
 function destruir()
 {
 //-- Recorre los blobs modificados que tienen un resource y los convierte a nombre de archivos
 foreach(array_keys($this->_blobs) as $id_fila) {
 foreach(array_keys($this->_blobs[$id_fila]) as $id_campo) {
 //-- Hay blob?
 if (isset($this->_blobs[$id_fila][$id_campo])) {
 //-- Si no se subio a un archivo
 if ($this->_blobs[$id_fila][$id_campo]['path'] == '') {
 $fp = $this->_blobs[$id_fila][$id_campo]['fp'];
 if (is_resource($fp)) {
 //-- Lo convierte a archivo
 rewind($fp);
 $temp_nombre = toba::proyecto()->get_path_temp().'/'.md5(uniqid(time()));
 $fpd = fopen($temp_nombre, 'w');
 stream_copy_to_stream($fp, $fpd);
 fclose($fpd);
 //-- Guarda el path
 $this->_blobs[$id_fila][$id_campo]['path'] = $temp_nombre;
 }
 }
 //--Borra la referencia al fp, ya esta subido al Sist.Archv
 $this->_blobs[$id_fila][$id_campo]['fp'] = null;
 } else {
 unset($this->_blobs[$id_fila][$id_campo]);
 }
 }
 }
 parent::destruir();
 }
 
 /**
 * @ignore
 */
 protected function activar_cargas_externas()
 {
 //--- Se recorren las cargas externas, el lugar ideal seria hacer esto en el ap, pero aca es mas simple y eficiente
 if ($this->_posee_columnas_ext) {
 foreach($this->_info_externas as $externa) {
 $parametros = array();
 $resultados = array();
 //-- Se identifican las columnas de esta carga
 foreach($this->_info_externas_col as $ext_col) {
 if ($ext_col['externa_id'] == $externa['externa_id']) {
 if ($ext_col['es_resultado'] == 1) {
 $resultados[] = $ext_col['columna'];
 } else {
 $parametros[] = $ext_col['columna'];
 }
 }
 }
 if ($externa['sql'] != '') {
 //---Caso SQL
 $this->persistidor()->activar_proceso_carga_externa_sql(
 $externa['sql'], $parametros, $resultados, $externa['sincro_continua'], $externa['dato_estricto']);
 } else {
 //---- Caso de carga mediante DAO se divide en 3 casos
 $this->definir_metodo_carga_dao($externa, $resultados, $parametros);
 }
 }
 }
 }
 
 protected function definir_metodo_carga_dao($externa, $resultados, $parametros)
 {
 //-- La carga se realiza por una clase consulta php
 if (isset($externa['carga_consulta_php']) && !is_null($externa['carga_consulta_php'])) {
 $this->persistidor()->activar_proceso_carga_externa_consulta_php(
 $externa['metodo'], $externa['carga_consulta_php'], $parametros, $resultados, $externa['sincro_continua'],
 $externa['dato_estricto'], $externa['permite_carga_masiva'], $externa['metodo_masivo']);
 
 }elseif (isset($externa['carga_dt']) && ($externa['carga_dt'] != '')) {    //--Se carga mediante datos_tabla
 $this->persistidor()->activar_proceso_carga_externa_datos_tabla(
 $externa['carga_dt'], $externa['metodo'],$parametros,
 $resultados, $externa['sincro_continua'], $externa['dato_estricto'], $externa['permite_carga_masiva'], $externa['metodo_masivo']);
 } else {        //Se carga con llamada estatica a una clase especifica.
 $this->persistidor()->activar_proceso_carga_externa_dao(
 $externa['metodo'], $externa['clase'], $externa['include'],
 $parametros, $resultados, $externa['sincro_continua'], $externa['dato_estricto'], $externa['permite_carga_masiva'], $externa['metodo_masivo']);
 }
 }
 
 /**
 * @ignore
 */
 protected function activar_control_valores_unicos()
 {
 foreach( $this->_info_valores_unicos as $regla ) {
 if(isset($regla['columnas'])) {
 $columnas = explode(',',$regla['columnas']);
 $columnas = array_map('trim', $columnas);
 $this->set_no_duplicado( $columnas );
 }
 }
 }
 
 function set_definicion_columna($columna, $propiedad, $valor)
 {
 $this->_columnas[$columna][$propiedad] = $valor;
 }
 
 /**
 * Reserva un id interno y lo retorna
 */
 function reservar_id_fila()
 {
 $actual = $this->_proxima_fila;
 $this->_proxima_fila++;
 return $actual;
 }
 
 /**
 * Retorna el proximo id interno a ser utilizado
 */
 function get_proximo_id()
 {
 return $this->_proxima_fila;
 }
 
 /**
 * Shorcut a toba::logger()->debug incluyendo infomación básica del componente
 */
 protected function log($txt)
 {
 toba::logger()->debug("TABLA: [{$this->get_tabla()}]\n".$txt, 'toba');
 }
 //-------------------------------------------------------------------------------
 //--  Relacion con otros ELEMENTOS
 //-------------------------------------------------------------------------------
 
 /**
 * Informa a la tabla que existe una tabla padre
 * @param toba_relacion_entre_tablas $relacion
 * @ignore
 */
 function agregar_relacion_con_padre($relacion, $id_padre)
 {
 $this->_relaciones_con_padres[$id_padre] = $relacion;
 }
 
 /**
 * Retorna las relaciones con las tablas padre
 * @return array de {@link toba_relacion_entre_tablas toba_relacion_entre_tablas}
 * @ignore
 */
 function get_relaciones_con_padres()
 {
 return $this->_relaciones_con_padres;
 }
 
 /**
 * Retorna la relación con una tabla padre
 * @return {@link toba_relacion_entre_tablas toba_relacion_entre_tablas}
 * @ignore
 */
 function get_relacion_con_padre($id_tabla_padre)
 {
 return $this->_relaciones_con_padres[$id_tabla_padre];
 }
 
 /**
 * Informa a la tabla que existe una tabla hija de la actual
 * @param toba_relacion_entre_tablas $relacion
 * @ignore
 */
 function agregar_relacion_con_hijo($relacion, $id_hijo)
 {
 $this->_relaciones_con_hijos[$id_hijo] = $relacion;
 }
 
 /*
 ***  Notificaciones  ***
 */
 
 private function notificar_contenedor($evento, $param1=null, $param2=null)
 {
 if(isset($this->controlador)){
 //$this->_contenedor->registrar_evento($this->_id, $evento, $param1, $param2);
 }
 }
 
 /**
 * Aviso a las relacion padres que el componente HIJO se CARGO
 * @ignore
 */
 function notificar_padres_carga($reg_hijos=null)
 {
 if(isset($this->_relaciones_con_padres)){
 foreach ($this->_relaciones_con_padres as $relacion) {
 $relacion->evt__carga_hijo($reg_hijos);
 }
 }
 }
 
 /**
 * Aviso a las relaciones hijas que el componente PADRE sincrozo sus actualizaciones
 * @ignore
 */
 function notificar_hijos_sincronizacion($filas=array())
 {
 if(isset($this->_relaciones_con_hijos)){
 foreach ($this->_relaciones_con_hijos as $relacion) {
 $relacion->evt__sincronizacion_padre($filas);
 }
 }
 }
 
 
 /**
 * Retorna la {@link toba_datos_relacion relacion} que contiene a esta tabla, si existe
 * @return toba_datos_relacion
 */
 function get_relacion()
 {
 if (isset($this->controlador)) {
 return $this->controlador;
 }
 }
 
 /**
 * Retorna un objeto en el cual se puede realizar busquedas complejas de registros en memoria
 * @return toba_datos_busqueda
 */
 function nueva_busqueda()
 {
 return new toba_datos_busqueda($this->controlador, $this);
 }
 
 //-------------------------------------------------------------------------------
 //-- Preguntas BASICAS
 //-------------------------------------------------------------------------------
 
 /**
 *    Retorna las columnas que son claves en la tabla
 */
 function get_clave()
 {
 if (empty($this->_clave)) {
 return null;
 }else{
 return $this->_clave;
 }
 }
 
 /**
 * Retorna el valor de la clave para un fila dada
 * @param mixed $id_fila Id. interno de la fila
 * @return array Valores de las claves para esta fila, en formato RecordSet
 */
 function get_clave_valor($id_fila)
 {
 $temp = array();
 foreach( $this->_clave as $columna ){
 $temp[$columna] = $this->get_fila_columna($id_fila, $columna);
 }
 return $temp;
 }
 
 /**
 * Retorna la cantidad maxima de filas que puede contener la tabla (si existe tal restriccion)
 * @return integer, 0 si no hay tope
 */
 function get_tope_max_filas()
 {
 return $this->_tope_max_filas;
 }
 
 
 /**
 * Retorna la cantidad minima de fila que debe contener la tabla (si existe tal restriccion)
 * @return integer, 0 si no hay tope
 */
 function get_tope_min_filas()
 {
 return $this->_tope_min_filas;
 }
 
 /**
 * Retorna la cantidad de filas que sufrieron cambios desde la carga, y por lo tanto se van a sincronizar
 * @return integer
 */
 function get_cantidad_filas_a_sincronizar()
 {
 $cantidad = 0;
 foreach(array_keys($this->_cambios) as $fila){
 if( ($this->_cambios[$fila]['estado'] == "d") ||
 ($this->_cambios[$fila]['estado'] == "i") ||
 ($this->_cambios[$fila]['estado'] == "u") ){
 $cantidad++;
 }
 }
 return $cantidad;
 }
 
 /**
 * Retorna lasfilas que sufrieron cambios desde la carga
 * @param array $cambios Combinación de tipos de cambio a buscar: d, i o u  (por defecto los tres)
 * @return array Ids. internos
 */
 function get_id_filas_a_sincronizar( $cambios=array("d","i","u") )
 {
 $ids = array();
 foreach(array_keys($this->_cambios) as $fila){
 if( in_array($this->_cambios[$fila]['estado'], $cambios) ){
 $ids[] = $fila;
 }
 }
 return $ids;
 }
 
 /**
 * Devuelve las fks que asocian a las tablas extendidas
 * @return array
 */
 function get_fks()
 {
 return $this->_info_fks;
 }
 
 //-------------------------------------------------------------------------------
 //-- Configuracion
 //-------------------------------------------------------------------------------
 
 /**
 * Cambia la cantidad maxima de filas que puede contener la tabla
 * @param integer $cantidad 0 si no hay tope
 */
 function set_tope_max_filas($cantidad)
 {
 if ($cantidad == '')
 $cantidad = 0;
 if(is_numeric($cantidad) && $cantidad >= 0){
 $this->_tope_max_filas = $cantidad;
 if ($cantidad != 1) {
 $this->set_es_unico_registro(false);
 }
 }else{
 throw new toba_error_def("El valor especificado en el TOPE MAXIMO de registros es incorrecto");
 }
 }
 
 /**
 * Cambia la cantidad mínima de filas que debe contener la tabla
 * @param integer $cantidad 0 si no hay tope
 */
 function set_tope_min_filas($cantidad)
 {
 if ($cantidad == '')
 $cantidad = 0;
 if(is_numeric($cantidad) && $cantidad >= 0){
 $this->_tope_min_filas = $cantidad;
 }else{
 throw new toba_error_def("El valor especificado en el TOPE MINIMO de registros es incorrecto");
 }
 }
 
 /**
 * Indica una combinacion de columnas cuyos valores no deben duplicarse (similar a un unique de sql)
 */
 function set_no_duplicado( $columnas )
 {
 $this->_no_duplicado[] = $columnas;
 }
 
 /**
 * Indica que la tabla maneja un único registro en memoria, habilitando la api get/set
 * @param boolean $unico
 */
 function set_es_unico_registro($unico)
 {
 $this->_es_unico_registro = $unico;
 }
 
 //-------------------------------------------------------------------------------
 //-- MANEJO DEL CURSOR INTERNO---------------------------------------------------
 //-------------------------------------------------------------------------------
 
 /**
 * Fija el cursor en una fila dada
 * Cuando la tabla tiene un cursor muchas de sus operaciones empiezan a tratar a esta fila como la única
 * y sus tablas padres e hijas también. Por ejemplo al pedir las filas de la tabla hija solo retorna aquellas filas hijas del registro cursor de la tabla padre.
 * @param mixed $id Id. interno de la fila
 */
 function set_cursor($id)
 {
 $id = $this->normalizar_id($id);
 if( $this->existe_fila($id) ){
 $this->_cursor_original = isset($this->_cursor) ? $this->_cursor : null;
 $this->_cursor = $id;
 $this->log("Nuevo cursor '{$this->_cursor}' en reemplazo del anterior '{$this->_cursor_original}'");
 }else{
 throw new toba_error_def($this->get_txt() . "La fila '$id' no es valida");
 }
 }
 
 /**
 * Deshace el ultimo seteo de cursor
 */
 function restaurar_cursor()
 {
 $this->_cursor = $this->_cursor_original;
 $this->log("Se restaura el cursor '{$this->_cursor_original}'");
 }
 
 
 /**
 * Asegura que el cursor no se encuentre posicionado en ninguna fila específica
 */
 function resetear_cursor()
 {
 unset($this->_cursor);
 $this->log("Se resetea el cursor");
 }
 
 /**
 * Retorna el Id. interno de la fila donde se encuentra actualmente el cursor de la tabla
 * @return mixed
 */
 function get_cursor()
 {
 if(isset($this->_cursor)){
 return $this->_cursor;
 }
 }
 
 /**
 * Hay una fila seleccionada por el cursor?
 */
 function hay_cursor()
 {
 return isset($this->_cursor);
 }
 
 //-------------------------------------------------------------------------------
 //-- ACCESO a FILAS   -----------------------------------------------------------
 //-------------------------------------------------------------------------------
 
 /**
 * Retorna el conjunto de filas que respeta las condiciones dadas
 * Por defecto la búsqueda es afectada por la presencia de cursores en las tablas padres.
 * @param array $condiciones Se utiliza este arreglo campo=>valor y se retornan los registros que cumplen (con condicion de igualdad) con estas restricciones. El valor no puede ser NULL porque siempre da falso
 * @param boolean $usar_id_fila Hace que las claves del array resultante sean
 * las claves internas del datos_tabla. Sino se usa una clave posicional y
 * la clave viaja en la columna apex_datos_clave_fila
 * @param boolean $usar_cursores Este conjunto de filas es afectado por la presencia de cursores en las tablas padres.
 * @return array Formato tipo RecordSet
 */
 function get_filas($condiciones=null, $usar_id_fila=false, $usar_cursores=true)
 {
 $datos = array();
 $a = 0;
 foreach( $this->get_id_fila_condicion($condiciones, $usar_cursores) as $id_fila ) {
 if($usar_id_fila){
 $datos[$id_fila] = $this->_datos[$id_fila];
 }else{
 $datos[$a] = $this->_datos[$id_fila];
 //esta columna indica cual fue la clave del registro
 $datos[$a][apex_datos_clave_fila] = $id_fila;
 }
 $a++;
 }
 return $datos;
 }
 
 /**
 * Retorna los ids de todas las filas (sin eliminar) de esta tabla
 * @param boolean $usar_cursores Este conjunto de filas es afectado por la presencia de cursores en las tablas padres
 * @return array()
 * @todo Se podría optimizar este método para no recaer en tantos recorridos
 */
 function get_id_filas($usar_cursores=true)
 {
 $coincidencias = array();
 foreach(array_keys($this->_cambios) as $id_fila){
 if($this->_cambios[$id_fila]['estado']!="d"){
 $coincidencias[] = $id_fila;
 }
 }
 if ($usar_cursores) {
 //Si algún padre tiene un cursor posicionado,
 //se restringe a solo las filas que son hijas de esos cursores
 foreach ($this->_relaciones_con_padres as $id => $rel_padre) {
 $coincidencias = $rel_padre->filtrar_filas_hijas($coincidencias);
 }
 }
 return $coincidencias;
 }
 
 /**
 * Retorna los ids de todas las filas (sin eliminar) de esta tabla
 * @param boolean $usar_cursores Este conjunto de filas es afectado por la presencia de cursores en las tablas padres
 * @return array()
 * @todo Se podría optimizar este método para no recaer en tantos recorridos
 */
 function get_id_filas_filtradas_por_cursor($incluir_eliminados=false)
 {
 if($this->hay_cursor()){
 return array( $this->get_cursor() );
 } else {
 $coincidencias = array();
 foreach(array_keys($this->_cambios) as $id_fila){
 if($incluir_eliminados || $this->_cambios[$id_fila]['estado']!="d"){
 $coincidencias[] = $id_fila;
 }
 }
 foreach ($this->_relaciones_con_padres as $id => $rel_padre) {
 $coincidencias = $rel_padre->filtrar_filas_hijas($coincidencias, $incluir_eliminados);
 }
 return $coincidencias;
 }
 }
 
 
 /**
 * Retorna los padres de un conjunto de registros especificos
 */
 function get_id_padres($ids_propios, $tabla_padre)
 {
 $salida = array();
 foreach ($ids_propios as $id_propio) {
 $id_padre = $this->get_id_fila_padre($tabla_padre, $id_propio);
 if ($id_padre !== null) {
 $salida[] = $id_padre;
 }
 }
 return array_unique($salida);
 }
 
 /**
 * Busca en una tabla padre el id de fila padre que corresponde a la fila hija especificada
 */
 function get_id_fila_padre($tabla_padre, $id_fila)
 {
 $id_fila = $this->normalizar_id($id_fila);
 if(!isset($this->_relaciones_con_padres[$tabla_padre])) {
 throw new toba_error_def("La tabla padre '$tabla_padre' no existe");
 }
 return $this->_relaciones_con_padres[$tabla_padre]->get_id_padre($id_fila);
 }
 
 
 /**
 * Busca los registros en memoria que cumplen una condicion.
 * Solo se chequea la condicion de igualdad. No se chequean tipos
 * @param array $condiciones Asociativo de campo => valor. El valor no puede ser NULL porque siempre da falso
 *              Para condiciones más complejas (no solo igualdad) puede ser array($columna, $condicion, $valor),
 *                 por ejemplo array(array('id_persona','>=',10),...)
 * @param boolean $usar_cursores Este conjunto de filas es afectado por la presencia de cursores en las tablas padres
 * @return array Ids. internos de las filas, pueden no estar numerado correlativamente
 */
 function get_id_fila_condicion($condiciones=null, $usar_cursores=true)
 {
 //En principio las coincidencias son todas las filas
 $coincidencias = $this->get_id_filas($usar_cursores);
 //Si hay condiciones, se filtran estas filas
 if(isset($condiciones)){
 if(!is_array($condiciones)){
 throw new toba_error("Las condiciones de filtrado deben ser un array asociativo");
 }
 //Controlo que todas los campos que se utilizan para el filtrado existan
 /*foreach( array_keys($condiciones) as $columna){
 
 }*/
 foreach($coincidencias as $pos => $id_fila){
 //Verifico las condiciones
 foreach( array_keys($condiciones) as $campo){
 if (is_array($condiciones[$campo])) {
 list($columna, $operador, $valor) = $condiciones[$campo];
 } else {
 $columna = $campo;
 $operador = '==';
 $valor = $condiciones[$campo];
 }
 if( !isset($this->_columnas[$columna]) ){
 throw new toba_error_def("El campo '$columna' no existe. No es posible filtrar por dicho campo");
 }
 if(!isset($this->_datos[$id_fila][$columna])) {
 // Es posible que una fila no posea una columa. Ej: una nueva fila no tiene la clave si esta es una secuencia.
 // Si el valor no existe, considero que la comparacion con esa fila da falso (* != NULL)
 unset($coincidencias[$pos]);
 break;
 } else {
 if (! comparar($this->_datos[$id_fila][$columna], $operador, $valor)) {
 //Se filtra la fila porque no cumple las condiciones
 unset($coincidencias[$pos]);
 break;
 }
 }
 }
 }
 }
 return array_values($coincidencias);
 }
 
 /**
 * Retorna el contenido de una fila, a partir de su clave interna
 * @param mixed $id Id. interno de la fila en memoria
 * @return array columna => valor. En caso de no existir la fila retorna NULL
 */
 function get_fila($id)
 {
 $id = $this->normalizar_id($id);
 if(isset($this->_datos[$id])){
 $temp = $this->_datos[$id];
 $temp[apex_datos_clave_fila] = $id;    //incorporo el ID del dbr
 return $temp;
 }else{
 return null;
 //throw new toba_error("Se solicito un registro incorrecto");
 }
 }
 
 /**
 * Retorna el valor de una columna en una fila dada
 * @param mixed $id Id. interno de la fila
 * @param string $columna Nombre de la columna
 * @return mixed En caso de no existir, retorna NULL
 */
 function get_fila_columna($id, $columna)
 {
 $id = $this->normalizar_id($id);
 if(isset($this->_datos[$id][$columna])){
 return  $this->_datos[$id][$columna];
 }else{
 return null;
 }
 }
 
 /**
 * Retorna los valores de una columna específica
 * El conjunto de filas utilizado es afectado por la presencia de cursores en las tablas padres
 * @param string $columna Nombre del campo o columna
 * @return array Arreglo plano de valores
 */
 function get_valores_columna($columna)
 {
 $temp = array();
 foreach($this->get_id_filas() as $fila){
 $temp[] = $this->_datos[$fila][$columna];
 }
 return $temp;
 }
 
 /**
 * Retorna el valor de la columna de la fila actualmente seleccionada como cursor
 * @param string $columna Id. de la columna que contiene el valor a retornar
 * @return mixed NULL si no cursor o no hay filas
 */
 function get_columna($columna)
 {
 if ($this->get_cantidad_filas() == 0) {
 return null;
 } elseif ($this->hay_cursor()) {
 return $this->get_fila_columna($this->get_cursor(), $columna);
 } else {
 throw new toba_error_def("No hay posicionado un cursor en la tabla, no es posible determinar la fila actual");
 }
 }
 
 /**
 * Cantidad de filas que tiene la tabla en memoria
 * El conjunto de filas utilizado es afectado por la presencia de cursores en las tablas padres
 * @return integer
 */
 function get_cantidad_filas($usar_cursores=true)
 {
 return count($this->get_id_filas($usar_cursores));
 }
 
 /**
 * Existe una determina fila? (la fila puede estar marcada como para borrar)
 * @param mixed $id Id. interno de la fila
 * @return boolean
 */
 function existe_fila($id)
 {
 $id = $this->normalizar_id($id);
 if(! isset($this->_datos[$id]) ){
 return false;
 }
 if($this->_cambios[$id]['estado']=="d"){
 return false;
 }
 return true;
 }
 
 /**
 * Busca los registros en memoria que cumplen una condicion.
 * Solo se chequea la condicion de igualdad. No se chequean tipos
 * @param array $condiciones Asociativo de campo => valor.
 *              Para condiciones más complejas (no solo igualdad) puede ser array($columna, $condicion, $valor),
 *                 por ejemplo array(array('id_persona','>=',10),...)
 * @param boolean $usar_cursores Este conjunto de filas es afectado por la presencia de cursores en las tablas padres
 */
 function existe_fila_condicion($condiciones, $usar_cursores = true)
 {
 $ids = $this->get_id_fila_condicion($condiciones, $usar_cursores);
 return !empty($ids);
 }
 
 /**
 * Valida un id interno y a la vez permite aceptarlo como parte de un arreglo en
 * la columna apex_datos_clave_fila
 * @ignore
 */
 protected function normalizar_id($id)
 {
 if(!is_array($id)){
 return $id;
 }else{
 if(isset($id[apex_datos_clave_fila])){
 return $id[apex_datos_clave_fila];
 }
 }
 throw new toba_error_def($this->get_txt() . ' La clave tiene un formato incorrecto.');
 }
 
 //-------------------------------------------------------------------------------
 //-- ALTERACION de FILAS  ------------------------------------------------------
 //-------------------------------------------------------------------------------
 
 /**
 * Crea una nueva fila en la tabla en memoria
 *
 * @param array $fila Asociativo campo=>valor a insertar
 * @param mixed $ids_padres Asociativo padre =>id de las filas padres de esta nueva fila,
 *                           en caso de que no se brinde, se utilizan los cursores actuales en estas tablas padres
 * @param integer $id_nuevo Opcional. Id interno de la nueva fila, si no se especifica (recomendado)
 *                                 Se utiliza el proximo id interno.
 * @return mixed Id. interno de la fila creada
 */
 function nueva_fila($fila=array(), $ids_padres=null, $id_nuevo=null)
 {
 $this->notificar_contenedor("ins", $fila);
 //Saco el campo que indica la posicion del registro
 if(isset($fila[apex_datos_clave_fila])) unset($fila[apex_datos_clave_fila]);
 if(isset($fila[apex_ei_analisis_fila])) unset($fila[apex_ei_analisis_fila]);
 
 $this->validar_fila($fila);
 //SI existen columnas externas, completo la fila con las mismas
 if($this->_posee_columnas_ext){
 $campos_externos = $this->persistidor()->completar_campos_externos_fila($fila,"ins");
 foreach($campos_externos as $id => $valor) {
 $fila[$id] = $valor;
 }
 }
 
 //---Se le asigna un id a la fila
 if (!isset($id_nuevo) || $id_nuevo < $this->_proxima_fila) {
 $id_nuevo = $this->_proxima_fila;
 }
 $this->_proxima_fila = $id_nuevo + 1;
 //Se notifica a las relaciones del alta
 foreach ($this->_relaciones_con_padres as $padre => $relacion) {
 $id_padre = null;
 if (isset($ids_padres[$padre])) {
 $id_padre = $ids_padres[$padre];
 }
 $relacion->asociar_fila_con_padre($id_nuevo, $id_padre);
 }
 
 //Se agrega la fila
 $this->_datos[$id_nuevo] = $fila;
 $this->registrar_cambio($id_nuevo,"i");
 
 return $id_nuevo;
 }
 
 /**
 * Modifica los valores de una fila de la tabla en memoria
 * Solo se modifican los valores de las columnas enviadas y que realmente cambien el valor de la fila.
 * @param mixed $id Id. interno de la fila a modificar
 * @param array $fila Contenido de la fila, en formato columna=>valor, puede ser incompleto
 * @param array $nuevos_padres Arreglo (id_tabla_padre => $id_fila_padre, ....), solo se cambian los padres que se pasan por parámetros
 *                 El resto de los padres sigue con la asociación anterior
 * @return mixed Id. interno de la fila modificada
 */
 function modificar_fila($id, $fila, $nuevos_padres=null)
 {
 $id = $this->normalizar_id($id);
 if (!$this->existe_fila($id)){
 $mensaje = $this->get_txt() . " MODIFICAR. No existe un registro con el INDICE indicado ($id)";
 toba::logger()->error($mensaje);
 throw new toba_error_def($mensaje);
 }
 //Saco el campo que indica la posicion del registro
 if(isset($fila[apex_datos_clave_fila])) unset($fila[apex_datos_clave_fila]);
 if(isset($fila[apex_ei_analisis_fila])) unset($fila[apex_ei_analisis_fila]);
 
 $this->validar_fila($fila, $id);
 $this->notificar_contenedor("pre_modificar", $fila, $id);
 
 //Actualizo los valores
 $alguno_modificado = false;
 foreach(array_keys($fila) as $clave){
 $modificar = $this->es_campo_modificado($clave, $id, $fila);
 if ($modificar) {
 $alguno_modificado = true;
 $this->_datos[$id][$clave] = $fila[$clave];
 }
 }
 //--- Esto evita propagar cambios que en realidad no sucedieron
 if ($alguno_modificado) {
 if($this->_cambios[$id]['estado']!="i"){
 $this->registrar_cambio($id,"u");
 }
 
 /*
 Como los campos externos pueden necesitar un campo que no entrego la
 interfaz, primero actualizo los valores y despues tomo la fila y la
 proceso con la actualizacion de campos externos
 */
 //Si la tabla posee campos externos, le pido la nueva fila al persistidor
 if($this->_posee_columnas_ext){
 $campos_externos = $this->persistidor()->completar_campos_externos_fila($this->_datos[$id],"upd");
 foreach($campos_externos as $clave => $valor){
 $this->_datos[$id][$clave] = $valor;
 }
 }
 }
 $this->notificar_contenedor("post_modificar", $fila, $id);
 if (isset($nuevos_padres)) {
 $this->cambiar_padre_fila($id, $nuevos_padres);
 }
 return $id;
 }
 
 /**
 * Cambia los padres de una fila
 * @param mixed $id_fila
 * @param array $nuevos_padres Arreglo (id_tabla_padre => $id_fila_padre, ....), solo se cambian los padres que se pasan por parámetros
 *                 El resto de los padres sigue con la asociación anterior
 */
 function cambiar_padre_fila($id_fila, $nuevos_padres)
 {
 $id = $this->normalizar_id($id_fila);
 if (!$this->existe_fila($id)){
 $mensaje = $this->get_txt() . " CAMBIAR PADRE. No existe un registro con el INDICE indicado ($id)";
 toba::logger()->error($mensaje);
 throw new toba_error_def($mensaje);
 }
 $cambio_padre = false;
 foreach ($nuevos_padres as $tabla_padre => $id_padre) {
 if (!isset($this->_relaciones_con_padres[$tabla_padre])) {
 $mensaje = $this->get_txt() . " CAMBIAR PADRE. No existe una relación padre $tabla_padre.";
 throw new toba_error_def($mensaje);
 }
 if ($this->_relaciones_con_padres[$tabla_padre]->set_padre($id_fila, $id_padre)) {
 $cambio_padre = true;
 }
 }
 //-- Si algun padre efectivamente cambio, tengo que marcar al registro como actualizado
 if ($cambio_padre) {
 if($this->_cambios[$id_fila]['estado']!="i"){
 $this->registrar_cambio($id_fila,"u");
 }
 }
 }
 
 /**
 * Elimina una fila de la tabla en memoria
 * En caso de que la fila sea el cursor actual de la tabla, este ultimo se resetea
 * @param mixed $id Id. interno de la fila a eliminar
 * @return Id. interno de la fila eliminada
 */
 function eliminar_fila($id)
 {
 $id = $this->normalizar_id($id);
 if (!$this->existe_fila($id)) {
 $mensaje = $this->get_txt() . " ELIMINAR. No existe un registro con el INDICE indicado ($id)";
 toba::logger()->error($mensaje);
 throw new toba_error_def($mensaje);
 }
 if ( $this->get_cursor() == $id ) {
 $this->resetear_cursor();
 }
 $this->notificar_contenedor("pre_eliminar", $id);
 //Se notifica la eliminación a las relaciones
 foreach ($this->_relaciones_con_hijos as $rel) {
 $rel->evt__eliminacion_fila_padre($id);
 }
 foreach ( $this->_relaciones_con_padres as $rel) {
 $rel->evt__eliminacion_fila_hijo($id);
 }
 if($this->_cambios[$id]['estado']=="i"){
 unset($this->_cambios[$id]);
 unset($this->_datos[$id]);
 }else{
 $this->registrar_cambio($id,"d");
 }
 $this->notificar_contenedor("post_eliminar", $id);
 return $id;
 }
 
 /**
 * Elimina todas las filas de la tabla en memoria
 * @param boolean $con_cursores Tiene en cuenta los cursores del padre para afectar solo sus filas hijas, por defecto utiliza cursores.
 */
 function eliminar_filas($con_cursores = true)
 {
 foreach($this->get_id_filas($con_cursores) as $fila) {
 $this->eliminar_fila($fila);
 }
 }
 
 /**
 * Cambia el valor de una columna de una fila especifica
 *
 * @param mixed $id Id. interno de la fila de la tabla en memoria
 * @param string $columna Columna o campo de la fila
 * @param mixed $valor Nuevo valor
 */
 function set_fila_columna_valor($id, $columna, $valor)
 {
 $id = $this->normalizar_id($id);
 if( $this->existe_fila($id) ){
 if( isset($this->_columnas[$columna]) ){
 $this->modificar_fila($id, array($columna => $valor));
 }else{
 throw new toba_error_def("La columna '$columna' no es valida");
 }
 }else{
 throw new toba_error_def("La fila '$id' no es valida");
 }
 }
 
 /**
 * Cambia el valor de una columna en todas las filas
 *
 * @param string $columna Nombre de la columna a modificar
 * @param mixed $valor Nuevo valor comun a toda la columna
 * @param boolean $con_cursores Tiene en cuenta los cursores del padre para afectar sus filas hijas, por defecto no
 */
 function set_columna_valor($columna, $valor, $con_cursores=false)
 {
 if(! isset($this->_columnas[$columna]) ) {
 throw new toba_error_def("La columna '$columna' no es valida");
 }
 foreach($this->get_id_filas($con_cursores) as $fila) {
 $this->modificar_fila($fila, array($columna => $valor));
 }
 }
 
 /**
 * Procesa los cambios masivos de filas
 *
 * El id de la fila se asume que la key del registro o la columna apex_datos_clave_fila
 * Para procesar es necesario indicar el estado de cada fila utilizando una columna referenciada con la constante 'apex_ei_analisis_fila' los valores pueden ser:
 *  - 'A': Alta
 *  - 'B': Baja
 *  - 'M': Modificacion
 *
 * @param array $filas Filas en formato RecordSet, cada registro debe contener un valor para la constante apex_ei_analisis_fila
 * @param mixed $ids_padres Asociativo padre =>id de las filas padres de esta nueva fila,
 *                           en caso de que no se brinde, se utilizan los cursores actuales en estas tablas padres
 */
 function procesar_filas($filas, $ids_padres=null)
 {
 toba_asercion::es_array($filas,"toba_datos_tabla - El parametro no es un array.");
 //--- Controlo estructura
 foreach(array_keys($filas) as $id){
 if(!isset($filas[$id][apex_ei_analisis_fila])){
 throw new toba_error_def("Para procesar un conjunto de registros es necesario indicar el estado ".
 "de cada uno utilizando una columna referenciada con la constante 'apex_ei_analisis_fila'.
 Si los datos provienen de un ML, active la opción de analizar filas.");
 }
 }
 //--- Se asume que el id de la fila es la key del registro o la columna apex_datos_clave_fila.
 foreach ($filas as $id => $fila) {
 $id_explicito = false;
 if (isset($fila[apex_datos_clave_fila])) {
 $id = $fila[apex_datos_clave_fila];
 $id_explicito = true;
 }
 $accion = $fila[apex_ei_analisis_fila];
 unset($fila[apex_ei_analisis_fila]);
 switch($accion){
 case "A":
 //--- Si el ML notifico explicitamente el id, este es el id de la nueva fila, sino usa el mecanismo interno
 $nuevo_id = ($id_explicito) ? $id : null;
 $this->nueva_fila($fila, $ids_padres, $nuevo_id);
 break;
 case "B":
 $this->eliminar_fila($id);
 break;
 case "M":
 $this->modificar_fila($id, $fila);
 break;
 }
 }
 }
 
 //-------------------------------------------------------------------------------
 //-- Simplificación sobre una sola línea
 //-------------------------------------------------------------------------------
 
 /**
 * Cambia el contenido de la fila donde se encuentra el cursor interno
 * Si la tabla se definio admitiendo a lo sumo un registro, este cursor se posiciona automáticamente en la carga, sino se debe explicitar con el método set_cursor
 * En caso que no existan filas, se crea una nueva y se posiciona el cursor en ella
 * Si la fila es null, se borra la fila actual
 *
 * @param array $fila Contenido total o parcial de la fila a crear o modificar (si es null borra la fila actual)
 */
 function set($fila)
 {
 if($this->hay_cursor()){
 if (isset($fila)) {
 $this->modificar_fila($this->get_cursor(), $fila);
 } else {
 $this->eliminar_fila($this->get_cursor());
 }
 } else {
 if (isset($fila)) {
 $id = $this->nueva_fila($fila);
 $this->set_cursor($id);
 }
 }
 }
 
 /**
 * Retorna el contenido de la fila donde se encuentra posicionado el cursor interno
 * Si la tabla se definio admitiendo a lo sumo un registro, este cursor se posiciona automáticamente en la carga, sino se debe explicitar con el método set_cursor
 * En caso de que no haya registros retorna NULL
 */
 function get()
 {
 if ($this->get_cantidad_filas() == 0) {
 return null;
 } elseif ($this->hay_cursor()) {
 return $this->get_fila($this->get_cursor());
 } else {
 throw new toba_error_def("No hay posicionado un cursor en la tabla, no es posible determinar la fila actual");
 }
 }
 
 
 //---------------------------------------------------------------------------
 //-- Trabajo con campos BLOBs  ----------------------------------------------
 //---------------------------------------------------------------------------
 
 /**
 * Almacena un 'file pointer' en un campo binario o blob de la tabla.
 * @param string $columna Nombre de la columna binaria-blob
 * @param resource $blob file pointer o null en caso de querer borrar el valor
 * @param mixed $id_fila Id. interno de la fila que contiene la columna, en caso de ser vacio se utiliza el cursor
 */
 function set_blob($columna, $blob, $id_fila=null)
 {
 if (isset($blob) && ! is_resource($blob)) {
 throw new toba_error_def("Las columnas binarias o BLOB esperan un 'resource', producto generalmente de un 'fopen' del archivo a subir a la base");
 }
 if (! isset($id_fila)) {
 if ($this->hay_cursor()){
 $id_fila = $this->get_cursor();
 } else {
 throw new toba_error_def("No hay posicionado un cursor en la tabla, no es posible determinar la fila actual");
 }
 }
 //Borra algïñun cache previo
 if (isset($this->_blobs[$id_fila][$columna]['path']) && $this->_blobs[$id_fila][$columna]['path'] != '') {
 $path = $this->_blobs[$id_fila][$columna]['path'];
 if (file_exists($path)) {
 unlink($path);
 }
 }
 
 $this->_blobs[$id_fila][$columna] = array('fp'=>$blob, 'path'=>'',  'modificado' => true);
 if(isset($this->_cambios[$id_fila]['estado']) && $this->_cambios[$id_fila]['estado']!="i"){
 $this->registrar_cambio($id_fila,"u");
 }
 }
 
 /**
 * Retorna un 'file pointer' apuntando al campo binario o blob de la tabla.
 *
 * @param string $columna Nombre de la columna binaria-blob
 * @param mixed $id_fila Id. interno de la fila que contiene la columna, en caso de ser vacio se utiliza el cursor
 * @return resource
 */
 function get_blob($columna, $id_fila=null)
 {
 if (! isset($id_fila)) {
 if ($this->get_cantidad_filas() == 0) {
 return null;
 } elseif ($this->hay_cursor()) {
 $id_fila = $this->get_cursor();
 } else {
 throw new toba_error_def("No hay posicionado un cursor en la tabla, no es posible determinar la fila actual");
 }
 }
 if (!isset($this->_blobs[$id_fila][$columna])) {
 //-- Si no tiene el dato y es una fila nueva, no hay nada
 if ($this->_cambios[$id_fila]['estado'] == "i") {
 return null;
 }
 //-- Carga peresoza del file_pointer
 $fp = $this->persistidor()->consultar_columna_blob($id_fila, $columna);
 $this->_blobs[$id_fila][$columna] = array('fp' => $fp, 'path' => '', 'modificado' => false);
 }
 $fp = $this->_blobs[$id_fila][$columna]['fp'];
 $path = $this->_blobs[$id_fila][$columna]['path'];
 if (!is_resource($fp) && $path != '') {
 //Si no es un recurso el $fp, se carga desde el path previamente cargado
 $fp = fopen($path, 'rb');
 $this->_blobs[$id_fila][$columna]['fp'] = $fp;
 }
 return $fp;
 }
 
 /**
 * Permite obtener el fp de un blob que se haya modificado en esta transaccion
 * @ignore
 */
 function _get_blob_transaccion($id_registro, $col)
 {
 //-- Si no esta seteado, no se cargo ni modifico
 if (!isset($this->_blobs[$id_registro][$col])) {
 return null;
 } elseif ($this->_blobs[$id_registro][$col]['modificado']) {
 //--Hay que actualizar el BLOB
 $fp = $this->_blobs[$id_registro][$col]['fp'];
 $path = $this->_blobs[$id_registro][$col]['path'];
 if (! is_resource($fp)) {
 if ($path != '') {
 $fp = fopen($path, 'rb');
 if (! is_resource($fp)) {
 throw new toba_error_def("No fue posible recuperar el campo '$col' de la fila '$id_registro' desde archivo temporal '$fp'");
 }
 } else {
 // Quiere decir que explicitamente se lo hizo nulo
 return false;
 }
 }
 return $fp;
 
 } else {
 return null;
 }
 }
 
 //-------------------------------------------------------------------------------
 //-- VALIDACION en LINEA
 //-------------------------------------------------------------------------------
 
 /**
 * Valida un registro durante el procesamiento
 */
 private function validar_fila($fila, $id=null)
 {
 if(!is_array($fila)){
 throw new toba_error_def($this->get_txt() . ' La fila debe ser una array');
 }
 $this->evt__validar_ingreso($fila, $id);
 $this->control_estructura_fila($fila);
 }
 
 /**
 * Ventana de validacion que se invoca cuando se crea o modifica una fila en memoria. Lanzar una excepcion en caso de error
 * @param array $fila Datos de la fila
 * @param mixed $id Id. interno de la fila, si tiene (en el caso modificacion de la fila)
 *
 * @ventana
 */
 protected function evt__validar_ingreso($fila, $id=null){}
 
 //-------------------------------------------------------------------------------
 
 /**
 * Controla que los campos del registro existan
 * @ignore
 */
 protected function control_estructura_fila($fila)
 {
 foreach($fila as $campo => $valor){
 //SI el registro no esta en la lista de manipulables o en las secuencias...
 if( !(isset($this->_columnas[$campo]))  ){
 $mensaje = $this->get_txt() . get_class($this)." El registro tiene una estructura incorrecta: El campo '$campo' ".
 " no forma parte de la DEFINICION.";
 toba::logger()->warning($mensaje);
 }
 }
 }
 //-------------------------------------------------------------------------------
 
 /**
 * Controla que un registro no duplique los valores existentes
 */
 private function control_valores_unicos_fila($fila, $id=null)
 //Controla que un registro no duplique los valores existentes
 {
 if (isset($this->_no_duplicado)) {
 foreach($this->_no_duplicado as $columnas) {                //Para cada constraint de columnas
 $this->validar_columnas_en_fila($columnas, $fila, $id);
 }
 }
 }
 
 /**
 * Funcion que realiza la comparacion propiamente dicha de los datos
 * para el control de valores unicos. Esta separacion hace que la relacion entre
 * los distintos constraints sea un OR.. (antes era un AND)
 * @ignore
 */
 protected function  validar_columnas_en_fila($columnas, $fila, $id)
 {
 foreach(array_keys($this->_datos) as $id_fila) {                      //Recorro todos los datos cargados
 if (! is_null($id) && ($id_fila == $id)) continue;                        //Si es la misma fila no se procesa
 if ($this->_cambios[$id_fila]['estado'] != 'd') {                         //Si la fila no esta marcada para borrado
 $combinacion_existente = true;
 foreach($columnas as $columna) {                                    //Comparo los valores de las columnas del constraint
 if (isset($fila[$columna])) {
 $combinacion_existente = $combinacion_existente && ($fila[$columna] == $this->_datos[$id_fila][$columna]);
 }else{
 return;                                        //No existe valor para la columna, el constraint no salta
 }
 }
 if ($combinacion_existente) {
 throw new toba_error_validacion($this->get_txt().": Error de valores repetidos en columna '$columna'");
 }
 }
 }
 }
 
 //-------------------------------------------------------------------------------
 //-- VALIDACION global
 //-------------------------------------------------------------------------------
 
 /**
 * Validacion de toda la tabla necesaria previa a la sincronización
 */
 function validar($filas=array())
 {
 $this->control_tope_maximo_filas($this->get_cantidad_filas());
 $ids = $this->get_id_filas_a_sincronizar( array("u","i") );
 if(isset($ids)){
 foreach($ids as $id){
 if(count($filas)>0) {
 if (!in_array($id,$filas)) continue;
 }
 //$this->control_nulos($fila);
 $this->evt__validar_fila( $this->_datos[$id] );
 $this->control_valores_unicos_fila($this->_datos[$id], $id);
 }
 }
 $this->control_tope_minimo_filas();
 }
 
 /**
 * Ventana para hacer validaciones particulares previo a la sincronización
 * El proceso puede ser abortado con un toba_error, el mensaje se muestra al usuario
 * @param array $fila Asociativo clave-valor de la fila a validar
 *
 * @ventana
 */
 function evt__validar_fila($fila){}
 
 /*
 Controles previos a la sincronizacion
 Esto va a aca o en el AP??
 */
 /*
 private function control_nulos($fila)
 //Controla que un registro posea los valores OBLIGATORIOS
 {
 $mensaje_usuario = "El elemento posee valores incompletos";
 $mensaje_programador = $this->get_txt() . " Es necesario especificar un valor para el campo: ";
 if(isset($this->_campos_no_nulo)){
 foreach($this->_campos_no_nulo as $campo){
 if(isset($fila[$campo])){
 if((trim($fila[$campo])=="")||(trim($fila[$campo])=='NULL')){
 toba::logger()->error($mensaje_programador . $campo);
 throw new toba_error($mensaje_usuario . " ('$campo' se encuentra vacio)");
 }
 }else{
 toba::logger()->error($mensaje_programador . $campo);
 throw new toba_error($mensaje_usuario . " ('$campo' se encuentra vacio)");
 }
 }
 }
 }
 */
 /**
 * Valida que la cantidad de filas supere el mínimo establecido
 */
 protected function control_tope_minimo_filas()
 {
 if ($this->_tope_min_filas != 0 && $this->get_cantidad_filas() < $this->_tope_min_filas) {
 throw new toba_error_validacion("La tabla <em>{$this->_id_en_controlador}</em> requiere ingresar al menos {$this->_tope_min_filas} registro/s (se encontraron
 sólo {$this->get_cantidad_filas()}).");
 }
 }
 
 /**
 * Valida que la cantidad de filas a crear no supere el maximo establecido
 */
 protected function control_tope_maximo_filas($cantidad)
 {
 if (($this->_tope_max_filas != 0) && ($cantidad > $this->_tope_max_filas)) {
 throw new toba_error_validacion("No está permitido ingresar más de {$this->_tope_max_filas} registros
 en la tabla <em>{$this->_id_en_controlador}</em> (se encontraron $cantidad).");
 }
 }
 
 
 //-------------------------------------------------------------------------------
 //-- PERSISTENCIA  -------------------------------------------------------------
 //-------------------------------------------------------------------------------
 
 /**
 * Retorna el admin. de persistencia que asiste a este objeto durante la sincronización
 * @return toba_ap_tabla_db
 */
 function persistidor()
 {
 if(!isset($this->_persistidor)){
 if($this->_info_estructura['ap']=='0'){
 $clase = $this->_info_estructura['ap_sub_clase'];
 $include = $this->_info_estructura['ap_sub_clase_archivo'];
 if( (trim($clase) == '' ) ){
 throw new toba_error_def( $this->get_txt() . "Error en la definicion, falta definir la subclase");
 }
 }else{
 $clase = 'toba_'.$this->_info_estructura['ap_clase'];
 $include = $this->_info_estructura['ap_clase_archivo'];
 }
 if( ! class_exists($clase) ) {
 $punto = toba::puntos_montaje()->get_por_id($this->_info_estructura['punto_montaje']);
 $path  = $punto->get_path_absoluto().'/'.$include;
 require_once($path);
 }
 $this->_persistidor = new $clase( $this );
 if($this->_info_estructura['ap_modificar_claves']){
 $this->_persistidor->activar_modificacion_clave();
 }
 }
 return $this->_persistidor;
 }
 
 /**
 * @deprecated  Usar persistidor() a secas
 */
 function get_persistidor()
 {
 return $this->persistidor();
 }
 
 
 
 /**
 * Carga la tabla restringiendo POR valores especificos de campos
 * Si los datos contienen una unica fila, esta se pone como cursor de la tabla
 */
 function cargar($clave=array())
 {
 return $this->persistidor()->cargar_por_clave($clave);
 }
 
 /**
 * La tabla esta cargada con datos?
 * @return boolean
 */
 function esta_cargada()
 {
 return $this->_cargada;
 }
 
 /**
 * Carga la tabla en memoria con un nuevo set de datos (se borra todo estado anterior)
 * Si los datos contienen una unica fila, esta se pone como cursor de la tabla
 * @param array $datos en formato RecordSet
 */
 function cargar_con_datos($datos)
 {
 $this->log("Carga de datos  FILAS: " . count($datos));
 $this->_datos = null;
 //Controlo que no se haya excedido el tope de registros
 $this->control_tope_maximo_filas(count($datos));
 
 $this->_datos = $datos;
 
 //Genero la estructura de control de cambios
 $this->generar_estructura_cambios();
 //Actualizo la posicion en que hay que incorporar al proximo registro
 $this->_proxima_fila = count($this->_datos);
 //Marco la tabla como cargada
 $this->_cargada = true;
 //Si es una unica fila se pone como cursor de la tabla
 if (count($datos) == 1 && $this->_es_unico_registro) {
 $this->_cursor = 0;
 }
 //Disparo la actulizacion de los mapeos con las tablas padres
 $this->notificar_padres_carga();
 }
 
 /**
 * Agrega a la tabla en memoria un nuevo set de datos (conservando el estado anterior).
 * Se asume que el set de datos llega desde el mecanismo de persistencia.
 *
 * @param array $datos en formato RecordSet
 * @param boolean $usar_cursores Los datos cargados se marcan como hijos de los cursores actuales en las tablas padre, sino son hijos del padre que tenia en la base
 * @return array Ids. internos de los datos anexados
 */
 function anexar_datos($datos, $usar_cursores=true)
 {
 $this->log("Anexado de datos  FILAS: " . count($datos) );
 //Controlo que no se haya excedido el tope de registros
 $this->control_tope_maximo_filas(count($this->get_id_filas(false)) + count($datos));
 
 //Agrego las filas
 $hijos = array();
 foreach( $datos as $fila ) {
 $this->_datos[$this->_proxima_fila] = $fila;
 if ($usar_cursores) {
 //Se notifica a las relaciones a los padres.
 foreach ($this->_relaciones_con_padres as $padre => $relacion) {
 $this->log("Anexado de datos: SET mapeo $padre");
 $relacion->asociar_fila_con_padre($this->_proxima_fila, null);
 }
 }
 $hijos[] = $this->_proxima_fila;
 $this->_proxima_fila++;
 }
 $this->regenerar_estructura_cambios($hijos);
 //Marco la tabla como cargada
 $this->_cargada = true;
 if (! $usar_cursores) {
 //Disparo la actulizacion de los mapeos con las tablas padres
 $this->log("Mapear cursores [". implode(',',$hijos) ."] a padres.");
 $this->notificar_padres_carga($hijos);
 }
 return $hijos;
 }
 
 /**
 * Sincroniza la tabla en memoria con el medio físico a travéz del administrador de persistencia.
 *
 * @return integer Cantidad de registros modificados en el medio
 */
 function sincronizar($usar_cursores=false)
 {
 if($usar_cursores) {
 $filas = $this->get_id_filas_filtradas_por_cursor(false);
 if($filas) {    // Si los cursores no filtran registros, no sincronizo nada
 $this->validar($filas);
 $modif = $this->persistidor()->sincronizar($filas);
 //Regenero la estructura que mantiene los cambios realizados
 $this->notificar_fin_sincronizacion($filas);
 }
 } else {
 $this->validar();
 $modif = $this->persistidor()->sincronizar();
 //Regenero la estructura que mantiene los cambios realizados
 $this->notificar_fin_sincronizacion();
 }
 return $modif;
 }
 
 /**
 * Sincroniza un conjunto de filas de la tabla en memoria con el medio físico a travéz del administrador de persistencia.
 *
 * @return integer Cantidad de registros modificados en el medio
 */
 function sincronizar_filas($filas)
 {
 $this->validar($filas);
 $modif = $this->persistidor()->sincronizar($filas);
 $this->notificar_fin_sincronizacion($filas);
 return $modif;
 }
 
 
 /**
 * Elimina todas las filas de la tabla en memoria y sincroniza con el medio de persistencia
 */
 function eliminar_todo()
 {
 //Me elimino a mi
 $this->eliminar_filas(false);
 //Sincronizo con la base
 $this->persistidor()->sincronizar_eliminados();
 $this->resetear();
 }
 
 /**
 * @deprecated Desde 0.8.4, usar eliminar_todo()
 */
 function eliminar()
 {
 toba::logger()->obsoleto(__CLASS__, __METHOD__, "0.8.4", "Usar eliminar_todo");
 $this->eliminar_todo();
 }
 
 /**
 * Deja la tabla sin carga alguna, se pierden todos los cambios realizados desde la carga
 */
 function resetear()
 {
 $this->log("RESET!!");
 $this->_datos = array();
 $this->_cargada = false;
 $this->_cambios = array();
 $this->_proxima_fila = 0;
 $this->_where = null;
 $this->_from = null;
 //-- Borra los temporales creados
 foreach (array_keys($this->_blobs) as $fila) {
 foreach (array_keys($this->_blobs[$fila]) as $campo) {
 if (isset($this->_blobs[$fila][$campo]['path']) && $this->_blobs[$fila][$campo]['path'] != '') {
 $path = $this->_blobs[$fila][$campo]['path'];
 if (file_exists($path)) {
 unlink($path);
 }
 }
 }
 }
 $this->_blobs = array();
 foreach ($this->_relaciones_con_hijos as $rel_hijo) {
 $rel_hijo->resetear();
 }
 $this->resetear_cursor();
 }
 
 //-------------------------------------------------------------------------------
 //-- Comunicacion con el Administrador de Persistencia
 //-------------------------------------------------------------------------------
 
 /*--- Del AP a mi ---*/
 
 /**
 * El AP avisa que terminó la sincronización
 * @ignore
 */
 function notificar_fin_sincronizacion($filas=array())
 {
 $this->regenerar_estructura_cambios($filas);
 }
 
 /*--- De mi al AP ---*/
 
 /**
 * @ignore
 */
 function get_conjunto_datos_interno()
 {
 return $this->_datos;
 }
 
 /**
 * Retorna la estructura interna que mantiene registro de las modificaciones/altas/bajas producidas en memoria
 * @ignore
 */
 function get_cambios()
 {
 return $this->_cambios;
 }
 
 /**
 * Retorna el nombre de las columnas de esta tabla
 */
 function get_columnas()
 {
 return $this->_columnas;
 }
 
 /**
 * Retorna el nombre de la {@link toba_fuente_datos fuente de datos} utilizado por este componente
 * @return string
 */
 function get_fuente()
 {
 return $this->_info["fuente"];
 }
 
 /**
 * Nombre de la tabla que se representa en memoria
 */
 function get_tabla()
 {
 return $this->_info_estructura['tabla'];
 }
 
 /**
 * Devuelve el nombre de la tabla extendida
 */
 function get_tabla_extendida()
 {
 return $this->_info_estructura['tabla_ext'];
 }
 
 /**
 * Retorna el schema de BD sobre el que trabaja el datos_tabla
 * @return string
 */
 function get_schema()
 {
 if (isset($this->_info_estructura['esquema'])) {
 return $this->_info_estructura['esquema'];
 }
 return null;
 }
 
 function get_schema_ext()
 {
 if (isset($this->_info_estructura['esquema_ext'])) {
 return $this->_info_estructura['esquema_ext'];
 }
 return null;
 }
 
 
 /**
 * Retorna el alias utilizado para desambiguar la tabla en uniones tales como JOINs
 * Se toma el primero seteado de: el alias definido, el rol en la relación o el nombre de la tabla
 * @return string
 */
 function get_alias()
 {
 if (isset($this->_info_estructura['alias'])) {
 return $this->_info_estructura['alias'];
 } elseif (isset($this->_id_en_controlador)) {
 return $this->_id_en_controlador;
 } else {
 return $this->get_tabla();
 }
 }
 
 /**
 * La tabla posee alguna columna marcada como de 'carga externa'
 * Una columna externa no participa en la sincronización posterior, pero por necesidades casi siempre estáticas
 * necesitan mantenerse junto al conjunto de datos.
 * @return boolean
 */
 function posee_columnas_externas()
 {
 return $this->_posee_columnas_ext;
 }
 
 //-------------------------------------------------------------------------------
 //-- Manejo de la estructura de cambios
 //-------------------------------------------------------------------------------
 
 /**
 * @ignore
 */
 protected function generar_estructura_cambios($limpiar=true, $indice_datos=null)
 {
 //Genero la estructura de control
 if ($limpiar) {
 $this->_cambios = array();
 }
 if (! isset($indice_datos)) {
 $indice_datos = array_keys($this->_datos);
 }
 foreach($indice_datos as $dato){
 $this->_cambios[$dato]['estado']    = "db";
 $this->_cambios[$dato]['clave']        = $this->get_clave_valor($dato);
 $this->_cambios[$dato]['original']    = $this->_datos[$dato];
 }
 }
 
 /**
 * @ignore
 */
 protected function regenerar_estructura_cambios($filas=array())
 {
 //BORRO los datos eliminados
 if(!$filas) {
 foreach(array_keys($this->_cambios) as $cambio){
 if($this->_cambios[$cambio]['estado']=='d'){
 unset($this->_datos[$cambio]);
 }
 }
 $this->generar_estructura_cambios();
 } else {
 $this->generar_estructura_cambios(false, $filas);
 }
 }
 
 /**
 * Determina que todas las filas de la tabla son nuevas
 * @param boolean $usar_cursores Si esta seteado, solo se marcan como nuevas las filas marcadas por el cursor
 *
 */
 function forzar_insercion($usar_cursores=false, $filas=null)
 {
 if($usar_cursores) {
 $filas = $this->get_id_filas_filtradas_por_cursor();
 } else {
 if (!isset($filas)) {
 $filas = array_keys($this->_cambios);
 }
 }
 foreach( $filas as $fila) {
 $this->log("FORZAR INSERT en FILA: $fila");
 $this->registrar_cambio($fila, "i");
 }
 }
 
 /**
 * Fuerza una cambio directo a la estructura interna que mantiene registro de los cambios
 * @param mixed $fila Id. interno de la fila
 * @param string $estado
 */
 protected function registrar_cambio($fila, $estado)
 {
 $this->_cambios[$fila]['estado'] = $estado;
 }
 
 /**
 * Determina si los datos cargados en la tabla difieren de los datos existentes en la base al inicio de la transacción
 * @return boolean
 */
 function hay_cambios()
 {
 if ($this->get_cantidad_filas_a_sincronizar() > 0) {
 $altas_bajas = $this->get_id_filas_a_sincronizar(array('d','i'));
 if (! empty($altas_bajas)) {
 return true;
 }
 foreach ($this->get_id_filas_a_sincronizar(array('u')) as $id_fila) {
 if ($this->hay_cambios_fila($id_fila)) {
 return true;
 }
 }
 }
 return false;
 }
 
 /**
 * Retorna verdadero si algún valor de la tabla cambio desde el inicio de la transacción
 * @return boolean
 */
 function hay_cambios_fila($id_fila)
 {
 $cambios = $this->get_cambios_fila($id_fila);
 return ! empty($cambios);
 }
 
 /**
 * Calcula las diferencias entre el valor original de la fila al momento de carga y el valor actual
 * @return array Asociativo campo => array('anterior' => $anterior, 'actual' => $actual)
 */
 function get_cambios_fila($id_fila, $datos_ap = array())
 {
 $diferencias = array();
 $datos_viejos = $this->_cambios[$id_fila]['original'];
 if (empty($datos_ap)) {                                                //Si el administrador de persistencia no pide comparacion especifica
 $datos_nuevos = $this->_datos[$id_fila];    //utilizo los datos internos del datos_tabla.
 } else {
 $datos_nuevos = $datos_ap;
 }
 foreach ($this->_columnas as $campo => $col) {
 if (!$col['externa']) {
 if ($col['tipo'] != 'B') {
 if (! isset($datos_nuevos[$campo]) && isset($datos_viejos[$campo])) {
 $diferencias[$campo] = array('anterior' => $datos_viejos[$campo], 'actual' => null);
 } elseif (isset($datos_nuevos[$campo]) && !isset($datos_viejos[$campo])) {
 $diferencias[$campo] = array('anterior' => null, 'actual' => $datos_nuevos[$campo]);
 } elseif (! isset($datos_nuevos[$campo]) && ! isset($datos_viejos[$campo])) {
 //No hace nada
 } else {
 if (is_bool($datos_viejos[$campo])) {
 $datos_viejos[$campo] = $datos_viejos[$campo] ? 1 : 0;
 }
 if (is_bool($datos_nuevos[$campo])) {
 $datos_nuevos[$campo] = $datos_nuevos[$campo] ? 1 : 0;
 }
 //--- Comparacion por igualdad estricta con un cast a string
 if ($this->persistidor()->get_usar_trim()) {
 $modificar =  (trim((string) $datos_viejos[$campo]) !== trim((string) $datos_nuevos[$campo]));
 } else {
 $modificar =  (((string) $datos_viejos[$campo]) !== ((string) $datos_nuevos[$campo]));
 }
 if ($modificar) {
 $diferencias[$campo] = array('anterior' => $datos_viejos[$campo], 'actual' => $datos_nuevos[$campo]);
 }
 }
 } else {
 //Es binario, se modifico? En ese caso no decimos las diferencias
 if (isset($this->_blobs[$id_fila][$campo]) && $this->_blobs[$id_fila][$campo]['modificado']) {
 $diferencias[$campo] = array('anterior' => '?', 'actual' => '?');
 }
 }
 }
 }
 return $diferencias;
 }
 
 
 
 /**
 * Verifica si hubo cambios en los valores de un campo especifico
 * @param string $campo Nombre de la columna a comparar
 * @param integer $id_viejos Identificador de la fila con los datos viejos
 * @param array $datos_nuevos Arreglo con los datos nuevos
 * @return boolean
 */
 function es_campo_modificado($campo, $id_viejos, $datos_nuevos)
 {
 if (! isset($this->_columnas[$campo])) {
 return false;
 }
 if (isset($this->_datos[$id_viejos][$campo])) {
 $viejo = $this->_datos[$id_viejos][$campo];
 if (is_bool($viejo)) {
 $viejo = $viejo ? 1 : 0;
 }
 switch ($this->_columnas[$campo]['tipo']) {
 case 'N':
 //--- Comparacion por igualdad estricta con un cast a Float en caso de Tipo Numero
 $modificar = (float)$viejo !== (float)$datos_nuevos[$campo];
 break;
 default:
 //--- Comparacion por igualdad estricta con un cast a string
 if ($this->persistidor()->get_usar_trim()) {
 $modificar = (trim((string) $viejo) !== trim((string) $datos_nuevos[$campo]));
 } else  {
 $modificar = (((string) $viejo) !== ((string) $datos_nuevos[$campo]));
 }
 }
 } else {
 //--- Si antes era null, se modifica si ahora no es null! (y si es una columna valida)
 $modificar = isset($this->_columnas[$campo]) && isset($datos_nuevos[$campo]);
 }
 return $modificar;
 }
 
 /**
 * Agrega en un nodo xml los datos del registro seleccinado en la tabla por el cursor, como atributos del nodo
 * @param SimpleXMLElement $xml El objeto nodo xml al que se le van a agregar los atributos
 */
 function get_xml($xml)
 {
 // Recupera los datos del registro marcado por el cursor
 $datos = $this->get();
 
 // Para cada columna, la agrega como atributo del nodo
 foreach($datos as $clave => $valor){
 $xml->addAttribute($clave,utf8_encode($valor));
 }
 }
 
 }
 ?>
 |