<?php

/*
** Fishlib - a collection of utilities for db-driven applications in PHP
** Copyright (C) 2002  LTWD, LLC DBA The Madfish Group
**
** This library is free software; you can redistribute it and/or
** modify it under the terms of the GNU Lesser General Public
** License as published by the Free Software Foundation; either
** version 2.1 of the License, or (at your option) any later version.
**
** This library is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
** Lesser General Public License for more details.
**
** You should have received a copy of the GNU Lesser General Public
** License along with this library; if not, write to the Free Software
** Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
*/

require_once('fishutil.class.php');
require_once(
'fishargs.class.php');
require_once(
'fishdb.class.php');
if (!
defined('BASE_ON'))
    
define('BASE_ON'TRUE);
if (!
defined('BASE_OFF'))
    
define('BASE_OFF'FALSE);
class 
FishBase
{

    var 
$table NULL;
    var 
$idfields NULL;
    var 
$ids NULL;
    var 
$what NULL;
    var 
$fields NULL;
    var 
$appname 'test';
    var 
$dsnpath NULL;

    var 
$dbh NULL;
    var 
$error NULL;
    var 
$errors = array();

    var 
$numeric_fields = array();
    var 
$tableinfo NULL;

    function 
__construct()
    {
        
$args func_get_args();
        
$this->constructor($args);
    }
    function 
FishBase()
    {
        
$args func_get_args();
        
$this->constructor($args);
    }

    function 
constructor($args)
    {
        
$this->inner_build($args);
        if (
$this->idfields)
        {
            if (!
is_array($this->idfields))
                
$this->idfields = (array)$this->idfields;
            
$this->ids = array();
            foreach (
$this->idfields as $i => $f)
            {
                eval(
'$this->ids[$i] =& $this->'.$f.';');
            }
        }
    }

    function 
dbh()
    {
        static 
$dbh NULL;
        if (
$dbh === NULL)
        {
            if (isset(
$this))
            {
                
$appname $this->appname;
                
$dsnpath $this->dsnpath;
            }
            else
            {
                list(
$appname,$dsnpath) = FishUtil::array_key_value(
                    
get_class_vars(FishBase::my_class())
                    , array(
'appname','dsnpath')
                );
            }
            if (
is_array($appname))
            {
                
$host FishUtil::array_key_value(
                    
$_SERVER
                    
'HTTP_HOST'
                    
FishUtil::array_key_value(
                        
$_SERVER
                        
'HOST'
                    
)
                );
                if (isset(
$host) && isset($appname[$host]))
                {
                    
$realappname $appname[$host];
                }
                elseif (isset(
$appname['default']))
                {
                    
$realappname $appname['default'];
                }
                else
                {
                    
$realappname array_shift($appname);
                }
                
$appname $realappname;
            }
            
$dbh FishDB::db_connect(array(
                
'application' => $appname
                
'dsnpath' => $dsnpath
            
));
            if (
$dbh === NULL)
            {
                
user_error('Received NULL db connection'E_USER_ERROR);
                return 
FALSE;
            }
            
$dbh->setFetchMode(DB_FETCHMODE_ASSOC);
        }
        if (isset(
$this))
        {
            
$this->dbh =& $dbh;
        }
        return 
$dbh;
    }

    function 
build()
    {
        
$args func_get_args();
        
$this->inner_build($args);
    }

    function 
inner_build($args)
    {
        if (
count($args) == 0)
        {
            return;
        }
        
$simple array_keys(get_object_vars($this));
        
$defaults = array();
        
$p FishArgs::parse_arguments($args$simple$defaults);
        
$foundkeys array_intersect($simplearray_keys($p));
        foreach (
$foundkeys as $k)
        {
        
//    if (method_exists($this, $k))
        //    {
        //        FishError::trace('this = ', get_class($this), ' call function k=', $k, ' on v=', $v);
        //        call_user_func(array($this,$k),$v);
        //    }
        //    else
        //    {
//                FishError::trace('this = ', get_class($this), ' set property k=', $k, ' to v=', $v);
                
$this->$k $p[$k];
        
//    }
        
}
        
$this->fix_numerics();
    }

    function 
erase()
    {
        foreach (
$this->fields as $k)
        {
            
$this->$k NULL;
        }
    }

    function 
fetch_simple_query($table=NULL)
    {
        if (empty(
$table) && isset($this->table))
            
$table $this->table;
        return 
"select * from $table ";
    }

    function 
build_idwhere(&$bind$idfields=NULL$ids=NULL$op='=')
    {
        if (
$idfields === NULL)
            return 
'';
        
$wheres = array();
        
$idfields = (array)$idfields;
        
$ids = (array)$ids;
        foreach (
$idfields as $i => $f)
        {
            if (isset(
$ids[$i]))
            {
                
$v $ids[$i];
                if (
$v !== NULL && empty($v) && in_array($f$this->numeric_fields))
                    
$v 0;
                
$fop FishDB::nullop($v,$op);
                
$wheres[] = "$f $fop ?";
                
$bind[] = $v;
            }
        }
        return 
implode(' and '$wheres);
    }

    function 
fetch_simple($table,$idfields=NULL,$ids=NULL,$op='=')
    {
        
$result FALSE;
        
$query $this->fetch_simple_query($table);
        
$bind = array();
        
$where $this->build_idwhere($bind,$idfields,$ids);
        if (
$where)
            
$query .= " where $where ";
        
$this->dbh();
        
$result $this->dbh->query($query,$bind);
        return 
$result;
    }

    function 
fetch_record($table=NULL,$idfields=NULL,$ids=NULL)
    {
        
$result $this->fetch_simple($table,$idfields,$ids);
        if (!
$result)
        {
            return 
FALSE;
        }
        
$row $result->fetchRow(DB_FETCHMODE_ASSOC);
        
$result->free();
        return 
$row;
    }

    function 
fetch_from_db($ids=NULL)
    {
        if (
$ids !== NULL)
        {
            
$this->ids = (array)$ids;
        }
//        FishError::trace('table=', $this->table, ' idfields=', $this->idfields, ' ids=', $this->ids);
        
$check array_diff($this->ids, array(''));
        if (empty(
$check))
            return 
FALSE;
        
$this->dbh();
        
$row $this->fetch_record($this->table$this->idfields$this->ids);
//        FishError::trace('row=', $row, ' last_query=', $this->dbh->last_query);
        
if (!$row)
        {
            return 
FALSE;
        }
        
$this->build($row);
        return 
TRUE;
    }

    function 
fetch_all($table=NULL,$idfields=NULL,$ids=NULL,$op='=')
    {
        if (empty(
$table)) { $table $this->table; }
        if (empty(
$idfields)) { $idfields $this->idfields; }
        
$result $this->fetch_simple($table,$idfields,$ids,$op);
        if (!
$result)
        {
            return 
FALSE;
        }
        
$rows = array();
        while (
$row $result->fetchRow(DB_FETCHMODE_ASSOC))
        {
            
$lrow =& $rows;
            foreach (
$idfields as $f)
            {
                if (!isset(
$lrow[$row[$f]]))
                    
$lrow[$row[$f]] = array();
                
$lrow =& $lrow[$row[$f]];
            }
            
$lrow $row;
        }
        
$result->free();
        return 
$rows;
    }

    function 
delete_from_db($ids=NULL)
    {
        if (
$ids === NULL)
        {
            
$ids $this->ids;
        }
        elseif (!
is_array($ids))
        {
            
$ids = (array)$ids;
        }
        
$this->dbh();
        if (isset(
$this->history))
        {
            
$this->write_history('delete');
        }
        
$bind = array();
        
$query "delete from {$this->table} ";
        
$where $this->build_idwhere($bind$this->idfields$ids);
        if (!empty(
$where))
            
$query .= " where $where ";
        
$result $this->dbh->query($query,$bind);
//        FishError::trace('last query:', $this->dbh->last_query, ' result:', $result, ' query:', $query, ' bind:', $bind);
        
return $result;
    }

    function 
delete_record()
    {
        if (
is_array($this->ids) && array_sum($this->ids) > 0)
            return 
$this->delete_from_db();
        return 
TRUE;
    }

    function 
fix_numerics()
    {
        if (!
is_array($this->numeric_fields))
            
var_dump($this->numeric_fields);
        foreach (
$this->numeric_fields as $f)
        {
            
$this->$f str_replace(array('$',','),array('',''),$this->$f);
        }
    }

    function 
write_to_db($treat_as='update')
    {
        
$args func_get_args();
        
$this->inner_build($args);
        
$result FALSE;
        
$this->fix_numerics();
        if (
count($this->ids) == 1)
        {
             if (empty(
$this->ids[0]))
                
$result $this->create_record();
            else
                
$result $this->update_record();
        }
        else
        {
            
// treat as new === trust me, i've already deleted this
            
if ($treat_as == 'new')
                
$result $this->create_record();
            else
                
$result = ($this->delete_record() && $this->create_record());
        }
        return 
$result;
    }

    function 
tableinfo()
    {
        if (
$this->tableinfo === NULL)
        {
            
$result $this->dbh->query(
                
'select * from ! where 1=0'
                
, array($this->table)
            );
            
$this->tableinfo $result->tableInfo(DB_TABLEINFO_ORDER);
        }
        return 
$this->tableinfo;
    }

    function 
fieldlist()
    {
        if (
is_array($this->fields) && count($this->fields) > 0)
        {
            return 
$this->fields;
        }
        if (
$this->dbh() && $this->table)
        {
            
$this->tableinfo();
            
$this->fields array_keys($this->tableinfo['order']);
            return 
$this->fields;
        }
        return 
NULL;
    }

    function 
create_record()
    {
        
$this->dbh();
        if (
count($this->idfields) == 1)
        {
            
$sequence $this->what;
            if (isset(
$this->sequence))
            {
//                FishError::trace('class:', get_class($this), ' sequence is set: ', $this->sequence);
                
$sequence $this->sequence;
            }
            else
            {
//                FishError::trace('class:', get_class($this), ' sequence is not set: use ', $this->what);
            
}
            if (
$sequence)
            {
                
// because PEAR DB will always add '_seq', argh
                
$sequence preg_replace('/_seq$/'''$sequence);
                
$nextId $this->dbh->nextId($sequence);
                
$this->ids[0] = $nextId;
//                FishError::trace('nextId for what=', $sequence , ' nextId=', $nextId , ' ids=', $this->ids[0]);
            
}
        }
//        FishError::start_trace(__FILE__);
        
$data_fields $this->data_fields();
        
$fields array_keys($data_fields);
        
$bind array_values($data_fields);
        
$stmt $this->dbh->autoPrepare(
            
$this->table
            
$fields
            
DB_AUTOQUERY_INSERT
        
);
        
$result $this->dbh->execute($stmt,$bind);
//        FishError::trace('last query:', $this->dbh->last_query, ' result:', $result);
        
if (isset($this->history))
        {
            
$this->write_history('create');
        }
        return 
$result;
    }

    function 
data_fields()
    {
        if (
$this->idfields)
        {
            foreach (
$this->idfields as $i => $f)
            {
                if (!empty(
$this->ids[$i]) && !$this->$f)
                    
$this->$f $this->ids[$i];
                elseif (empty(
$this->ids[$i]) && $this->$f)
                    eval(
'$this->ids[$i] =& $this->'.$f.';');
            }
        }
        
$fields = array();
        foreach (
$this->fieldlist() as $f)
        {
            if (!isset(
$this->$f))
                continue;
            
$v $this->$f;
            if (
$v === NULL)
            {
                
// skip it
//                FishError::trace('null: f:', $f, ' v:', $v);
            
}
            elseif (
$v === '' && in_array($f$this->numeric_fields))
            {
                
// skip it
//                FishError::trace('empty numeric: f:', $f, ' v:', $v);
            
}
            else
            {
//                FishError::trace('not empty: f:', $f, ' v:', $v);
                
$fields[$f] = $v;
            }
        }
        return 
$fields;
    }

    function 
write_history($action='create')
    {
        
$hv $this->history['bind'];
        
array_unshift($hv,$action);
        
$where $this->build_idwhere($hv$this->idfields$this->ids);
        
$hq = <<<EOQ
insert into {$this->history['table']}
select *, ? as action 
{$this->history['query']}
from 
{$this->table}
where $where 
EOQ;
        
$this->dbh->query($hq,$hv);
    }

    function 
update_record($update_where=NULL,$update_values=NULL)
    {
        
$data_fields $this->data_fields();
        
$fields array_keys($data_fields);
        
$bind array_values($data_fields);
        
$this->dbh();
        if (isset(
$this->history))
        {
            
$this->write_history('update');
        }
        if (
$update_where === NULL)
        {
            
$update_where $this->build_idwhere(
                
$bind
                
$this->idfields
                
$this->ids
            
);
        }
        
$stmt $this->dbh->autoPrepare(
            
$this->table
            
$fields
            
DB_AUTOQUERY_UPDATE
            
$update_where
        
);
        if (
$update_values !== NULL)
        {
            
$bind array_merge($bind$update_values);
        }
        
$result $this->dbh->execute($stmt$bind);
//        FishError::trace('query:' , $this->dbh->last_query, ' stmt:', $stmt, ' bind:', $bind, ' fields:', $fields, ' table:', $this->table);
        
return $result;
    }

    function 
legal_values($id=NULL$use_values=NULL)
    {
        static 
$values NULL;
        if (
$use_values !== NULL)
        {
            
$values $use_values;
        }
        if (
$values === NULL)
        {
            
$values $this->fetch_all();
        }
        if (
$id !== NULL)
        {
            if (!isset(
$values[$id]))
            {
                return 
FALSE;
            }
            return 
$values[$id];
        }
        return 
$values;
    }

    function 
transaction($what=NULL,$how=NULL)
    {
        static 
$states = array();
        static 
$state NULL;

//        FishError::trace('what:', $what, ' how:', $how);
        
$this->dbh();
        if (
$what === BASE_ON)
        {
            if (
$state !== BASE_ON)
            {
//                FishError::trace('begin transaction');
                
$this->dbh->query('begin');
                
$state BASE_ON;
            }
            
array_push($statesBASE_ON);
        }
        elseif (
$what === BASE_OFF)
        {
            if (
$how !== 'commit')
            {
                
$how 'rollback';
            }
            if (
$how === 'rollback')
            {
                
$states = array();
            }
            else
            {
                
array_pop($states);
            }
            if (
$state !== BASE_OFF && count($states) == 0)
            {
//                FishError::trace('end transaction: how=', $how);
                
$this->dbh->query($how);
                
$state BASE_OFF;
            }
        }
        return 
$state;
    }

    function 
begin()
    {
        
FishBase::transaction(BASE_ON);
    }
    function 
commit()
    {
        
FishBase::transaction(BASE_OFF,'commit');
    }
    function 
rollback($error=NULL)
    {
        
FishBase::transaction(BASE_OFF,'rollback');
        if (
$error === NULL && isset($this) && isset($this->error))
        {
            
$error $this->error;
        }
        if (
$error)
        {
            
user_error($errorE_USER_ERROR);
        }
    }

    
// this is a workaround hack for the fact that you can't
    // properly do a class::method() call with call_user_func_array(). 
    
function call_method_hack($class,$method,$args)
    {
        
$i 0;
        
$ilist = array();
        foreach (
$args as $arg)
        {
            
$ilist[] = '$i'.$i;
            eval(
'$i'.$i++.' = $arg;');
        }
        
$result FALSE;
        eval(
'$result = '.$class.'::'.$method.'('.implode(',',$ilist).');');
        return 
$result;
    }

    function 
__get($property)
    {
        if (
method_exists($this$property))
        {
            return 
call_user_func(array($this$property));
        }
        
$class get_class($this);
        
user_error(
            
"No such property '$property' defined for class '$class'"
            
E_USER_WARNING
        
);
    }

    function 
__set($property,$value)
    {
        if (
method_exists($this$property))
        {
            return 
call_user_func(array($this$property), $value);
        }
        
$class get_class($this);
        
user_error(
            
"No such property '$property' defined for class '$class'"
            
E_USER_WARNING
        
);
    }

    
// placeholder functions - children don't seem to be able to use
    // these unless they are defined in the parent
    
function __call($method$args) { }
    function 
__destruct() { }
    function 
my_class()
    {
        
$bt debug_backtrace();
        
array_shift($bt);
        
$top array_shift($bt);
        return 
$top['class'];
    }

    function 
queries($entry=NULL)
    {
        static 
$queries NULL;
        if (
$queries === NULL || (is_array($entry) && count($entry) == 0))
        {
            
$queries = array();
        }
        elseif (
$entry !== NULL)
        {
            
$queries[] = $entry;
        }
        return 
$queries;
    }

    function 
start_queries($class='FishDB')
    {
        
$call = array();
        
$call[] = isset($this) ? $this $class;
        
$call[] = 'queries';
        
call_user_func($call, array());
    }

    function 
run_queries($class='FishDB')
    {
        
$call = array();
        
$call[] = isset($this) ? $this $class;
        
$call[] = 'dbh';
        
$dbh call_user_func($call);

        
$call = array();
        
$call[] = isset($this) ? $this $class;
        
$call[] = 'queries';
        
$queries call_user_func($call);

        if (
count($queries) > 0)
        {
            
$this->begin();
            foreach (
$queries as $qb)
            {
                if (
is_array($qb))
                {
                    
$q array_shift($qb);
                    
$b array_shift($qb);
                }
                else
                {
                    
$q $qb;
                    
$b = array();
                }
//                FishError::trace('qb=', $qb, ' q=', $q, ' b=', $b);
                
$result $dbh->query($q$b);
//                FishError::trace('last query: ', $dbh->last_query);
// print "<li>{$dbh->last_query}\n";
                
if ($dbh->isError($result))
                {
// print "<li>ERROR: result=";
// var_dump($result);
                    
$this->rollback();
                    return 
FALSE;
                }
            }
            
$this->commit();
        }
        return 
TRUE;
    }

    function 
mysql_index_fields($table)
    {
        
$rows $this->dbh->getAll("show index from $table");
        
$indexes = array();
        foreach (
$rows as $row)
        {
            if (
$row['Non_unique'] == 0)
            {
                
$indexes[$row['Key_name']][] = $row['Column_name'];
            }
        }
        if (isset(
$indexes['PRIMARY']))
            
$output $indexes['PRIMARY'];
        else
            
$output array_shift($indexes);
        return 
$output;
    }

    function 
postgres_index_fields($table)
    {
        
// get indexes for postgres
        
$query = <<<EOQ
select a.attname 
from pg_attribute a, pg_index i, pg_class r
where r.relname = ?
    and r.oid = i.indrelid
    and i.indexrelid = a.attrelid
    and i.indisprimary = 't'
order by a.attnum
EOQ;
        return 
$this->dbh->getCol($query0, array($table));
    }

    function 
postgres_is_view($table)
    {
        
$type $this->dbh->getOne(
            
'select relkind from pg_class where relname = ?'
            
, array($table)
        );
        if (
$type == 'v')
            return 
TRUE;
        return 
FALSE;
    }

    function 
is_view($table=NULL)
    {
        static 
$viewlist NULL;
        
$result FALSE;
        if (
$table === NULL)
            
$table $this->table;
        if (empty(
$table))
            return 
$result;
        if (
$this->dbh->phptype == 'pgsql')
        {
            
$result $this->postgres_is_view($table);
        }
        elseif (
$this->dbh->phptype == 'mysql')
        {
            
// not yet
            
return FALSE;
        }
        else
        {
            
// god help us
            
if ($viewlist === NULL)
            {
                
$result $this->dbh->getListOf('view');
                if (!
DB::isError($result))
                    
$viewlist $result;
            }
            
$result in_array($table$viewlist);
        }
        return 
$result;
    }

    function 
index_fields($table=NULL)
    {
        
$results = array();
        if (
$table === NULL)
            
$table $this->table;
        if (empty(
$table))
            return 
$results;
        if (
$this->dbh->phptype == 'pgsql')
        {
            
$results $this->postgres_index_fields($table);
        }
        elseif (
$this->dbh->phptype == 'mysql')
        {
            
$results $this->mysql_index_fields($table);
        }
        return 
$results;
    }
}
?>