tags:

views:

448

answers:

10

I have a very simple helper function to produce SET statement for traditional plain mysql driver usage:

function dbSet($fields) {
  $set='';
  foreach ($fields as $field) {
    if (isset($_POST[$field])) {
      $set.="`$field`='".mysql_real_escape_string($_POST[$field])."', ";
    }
  }
  return substr($set, 0, -2); 
}

used like this

$id = intval($_POST['id']);
$fields = explode(" ","name surname lastname address zip fax phone");
$_POST['date'] = $_POST['y']."-".$_POST['m']."-".$_POST['d'];
$query  = "UPDATE $table SET ".dbSet($fields)." stamp=NOW() WHERE id=$id";

it makes code quite DRY and easy but flexible at the same time.

I gotta ask if anyone willing to share a similar function, utilizing PDO prepared statements feature?

I am still in doubts, how to accomplish this.
Is there a straight and simple way to use PDO prepared statements to insert data? What form it should be? Query builder helper? Or insert query helper? What parameters it should take?

I hope it can be easy enough to be used as an answer here on SO. Because in the every topic we can see prepared statements usage recommendation, but there is not a single good example. Real life example, I mean. To type bind_param() 20 times is not a good programming style I believe. And even 20 question marks too.

+6  A: 

I usually have a class extending PDO, but my class is pretty custom. If I get it cleaned up and tested I will post it at a later time. Here is a solution to your system, however.

function dbSet($fields, &$values) {
    $set = '';
    $values = array();

    foreach ($fields as $field) {
        if (isset($_POST[$field])) {
            $set .= "`$field` = ?,";
            $values[] = $_POST[$field];
        }
    }

    return rtrim($set, ',');
}

$fields = explode(" ","name surname lastname address zip fax phone date");
$_POST['date'] = $_POST['y']."-".$_POST['m']."-"$_POST['d'];

$query  = "UPDATE $table SET ".dbSet($fields, $values).", stamp=NOW() WHERE id=?";
$values[] = $id;

$dbh->prepare($query);
$dbh->execute($values);  

This may not be perfect and could use tweaking. It takes into account that $dbh is setup with a PDO Connection. Pending any minor syntax issues I made, that should work.

EDIT

Really though, I think I would go for Doctrine ORM (or another ORM). As you setup the model and add all the validation there, then it is as simple as:

$table = new Table();
$table->fromArray($_POST);
$table->save();

That should populate the contents easily. This is of course with an ORM, like Doctrine.

UPDATED

Did some minor tweaks to the first code, such as putting isset back and using rtrim over substr. Going to work on providing a mock up of a PDO Extension class just gotta layout the way to do it and do some unit tests to make sure it works.

Brad F Jacobs
I'm still trying to grasp PDO, but don't you need to bind the variables? Can someone explain this to me?
Catfish
thanks, I had something like this in my mind. yeah we have to prepare $values array as well.
Col. Shrapnel
@catfish, setting an array of values to use in the `execute` function alleviates the need to bind the parameters, if you use the `?`, I cannot speak to the `:variable` as I have never used that method. The order of the array does matter, so be cautious of that.
Brad F Jacobs
@Catfish they are bound. we get a query like this `UPDATE users SET username=?, password=? stamp=NOW() WHERE id=?` and $values array that contains 3 elements. And then bind this array to this query
Col. Shrapnel
+1 that's how I do it too.
Fanis
Why have you changed the above `isset` to `empty`? People do want to change a value to the empty string for example.
nikic
No clue, but that is a good point and why validation logic needs to be mixed in.
Brad F Jacobs
A: 

Reference: http://stackoverflow.com/questions/60174/best-way-to-stop-sql-injection-in-php

$preparedStatement = $db->prepare('SELECT * FROM employees WHERE name = :name');
$preparedStatement->execute(array(':name' => $name));
$rows = $preparedStatement->fetchAll();
Phill Pafford
I think he was looking for a set way to modify his current code to work with PDO, not a "cookie cutter" response to how to use PDO.
Brad F Jacobs
why redefine the wheel? three lines is all it takes
Phill Pafford
I can find dummy examples myself. But these examples are not for real life. To write each field name **three** times is not a job I am dream about.
Col. Shrapnel
@premiso thanks, but I am not looking for exact replacement only. I am open to any other suggestions too, as long as it KISSable and DRY :)
Col. Shrapnel
+3  A: 

i would extend the Core PDO Class andd a method like so:

class Database extends PDO
{
    public function QueryFromPost($Query,$items)
    {
        $params = array();
        $Query .= ' WHERE ';

        foreach($items as $key => $default)
        {
             $Query .= ' :' . $key. ' = ' . $key;
             if(isset($_POST[$key]))
             {
                  $params[':' . $key] = $_POST[$key];
             }else
             {
                 $params[':' . $key] = $default;
             }
        }
        $s = $this->prepare($Query);
        return $s->execute($params);
    }
}

Then use like so

$db = new Database(/*..Default PDO Params*/);
$statement = $db->QueryFromPost('SELECT * FROM employees',array('type' => 'plc'));
foreach($preparedStatement->fetchAll() as $row)
{
    //...
}

But as its already been said that you should be VERY weary of what your trying to do, you need to validate your data, its been sanitized but you not validated.

RobertPitt
Well it's SELECT query, but i rather interested in INSERT query automation. I am validating input by using an array with allowed field names. It seems it's the only reliable method.
Col. Shrapnel
are you not using a frameowrk or any particular pattern ?
RobertPitt
I am open to use some framework, if I find it sensible and usable.
Col. Shrapnel
Well, I would advise with `CodeIgnighter` but i really hate its core loading structure, have you thought about making your own, its not terribly difficult?
RobertPitt
+1  A: 

I've been toying something together. You only need the topmost function, rip the remaining stuff out. And don't laugh about the name.
http://sourceforge.net/p/php7framework/svn/LATEST/tree/trunk/db.php?format=raw

Anyway; it provides some alternative prepared statement placeholders. Your example could be shortened into:

db("UPDATE table SET :, WHERE id=:id", $columns[], $where[]);

This case only works with named parameters, so $set would be array("name"=>..) and $where=array("id"=>123). The :, gets expanded on the first array you pass. It's replaced with comma-separated name=:name pairs (that's why its mnemonic is :,).

There are a few more placeholders :, :& :: and :? for different use cases. Only the ?? is really a standard. So it needs some getting used to, but it significantly simplifies prepared statements. Parameter passing is super flexible.

mario
Oh great. I'll take a look. I was thinking of something of this kind, because standard set of placeholders is a poor one. Imagine you need to make a IN statement out of some array. Too much handwork that can be easily automated by using special placeholder.
Col. Shrapnel
Exactly. This was the original use case that drove me to it. I've just been feature creeping it since then. `??` is the most versatile, that's why Perls DBIx uses it too. And string patching just seems the second best alternative to LINQ language support.
mario
@mario great concept and nice implementation. I will look into it more closely next week. Personally I just have a helper that creates placeholders for a query like @premiso does above, but this is more elegant and makes for a bit more readable sql in code.
Fanis
A: 

You can extend PDO like that:

class CustomPDO extends PDO {

    public function updateTable($sTable, array $aValues = array()){

        if (!empty($aValues) && !empty($sTable)){

            # validation of table / columns name
            $sTable = mysql_real_escape_string($sTable);

            $aColumns = array_map('mysql_real_escape_string',array_keys($aValues));

            $aElements = array();

            foreach ($aColumns as $sColumn){

                $aElements[] = "`$sColumn`= :$sColumn";

            } // foreach

            $sStatement = "UPDATE $sTable SET " . implode(',', $aElements);

            $oPDOStatement = $this->prepare($sStatement);

            if ($oPDOStatement){

                return $oPDOStatement->execute($aValues);

            } // if

        } // if

        return false;

    } // updateTable

}

# usage :
# $oDb->updateTable('tbl_name',$_POST);


# test

error_reporting (E_ALL);
ini_Set('display_errors',1);

$oDb = new CustomPDO('sqlite::memory:');

$oDb->exec('CREATE TABLE t1(c1 TEXT, c2 INTEGER)');

$oDb->exec("INSERT INTO t1(c1, c2) VALUES ('X1',1)");

var_dump($oDb->query('SELECT * FROM t1')->fetchAll(PDO::FETCH_ASSOC));

$oDb->updateTable('t1', array('c1'=>'f1','c2**2'=>2));

var_dump($oDb->query('SELECT * FROM t1')->fetchAll(PDO::FETCH_ASSOC));
ts
A: 

Like others I've extended the standard PDO class to suit my needs. Something along the lines of this may suit you:

Class ExtendedPDO extends PDO
{

    public function prepareArray($sql, array $data)
    {
        // Call the standard prepare method
        $statement = parent::prepare($sql);

        foreach ($data as $field=>$value) {
            $statement->bindValue(':' . $field, $value);
        }

        return $statement;
    }

}

Then you can use it quite simply:

// Include connection variables
include '../includes/config/database.php';

// The data to use in the query
$data = array(
    'title' => 'New value',
    'id'    => 1,
);

// The query you want to run
$sql = '
    UPDATE
        test
    SET
        title = :title
    WHERE
        id = :id
';

try {
    // Connect to the database
    $dbh = new ExtendedPDO(PDO_DSN, PDO_USERNAME, PDO_PASSWORD);

    // Attach the data to your query
    $stmt = $dbh->prepareArray($sql, $data);

    // Run it
    $stmt->execute();
} catch (PDO Exception $e) {
    echo $e->getMessage();
}
Nev Stokes
+1  A: 

Here's my general database abstraction class. Take a look at the autoExecute() function. It offers tons of flexibility for whatever it is you might want to accomplish. I should warn that this was written for PHP 5.3, and has been slightly tailored for PostgreSQL.

<?php
/**
 * Database abstraction and query result classes
 * Requires PHP 5.3
 *
 * Events:
 *  - on_commit - Dispatched when the transaction is successfully committed to the DB
 *  - on_rollback - Dispatched when the transaction is rolled back in the DB
 *
 * @author Kenaniah Cerny <[email protected]>
 * @version 1.1.2
 * @license http://creativecommons.org/licenses/by/3.0/us/
 * @copyright Copyright (c) 2009, Kenaniah Cerny
 */
class Database extends PDO {

    private $stmt;
    private $good_trans = null;
    private $nested_transactions = 0; //Keeps track of virtual transaction nesting level
    private $callbacks = array();

    private static $connections = array(); //Keeps track of opened connections

    /**
     * Returns a database instance using lazy instantiation
     * @param string $name a database connection name
     * @param array $config database config details for a new connection
     */
    static function getInstance($name = 'main', $config=array()){

        //Attempt to return an existing connection
        if(array_key_exists($name, self::$connections)):
            return self::$connections[$name];
        endif;

        //Attempt to create a new connection
        $host = in_array($config['host'], array('localhost', '127.0.0.1')) ? "" : ";host=" . $config['host'];
        $db = new Database($config['driver'].":dbname=".$config['name'].$host, $config['user'], $config['pass']);

        //Save to connection pool
        self::$connections[$name] = $db;

        return $db;

    }

    /**
     * Registers a callback to be run when the given event is invoked
     * @param string $event Event name
     * @param callable $callable
     */
    public function register_listener($event, $callable){

        if(!array_key_exists($event, $this->callbacks)):
            $this->callbacks[$event] = array($callable);
        else:
            $this->callbacks[$event][] = $callable;
        endif;

    }

    /**
     * Invokes callbacks for the given event type
     * @param string $event Event name
     * @param boolean $stop_on_false Stops bubbling this event if one of the handlers returns false
     */
    protected function dispatch_event($event, $stop_on_false = true){

        if(!array_key_exists($event, $this->callbacks)) return;

        foreach($this->callbacks[$event] as $callable):

            $res = call_user_func($callable, $this, $event);
            if($stop_on_false && $res === false) return false;

        endforeach;

        return true;

    }

    /**
     * PDO Constructor
     * @param $dsn
     * @param $username
     * @param $password
     */
    function __construct($dsn, $username, $password) {
        parent::__construct($dsn, $username, $password);
    }

    /**
     * Prepares an SQL statement
     * @param string $sql
     */
    function prepare($sql) {
        $stmt = parent::prepare($sql, array(PDO::ATTR_STATEMENT_CLASS => array(__NAMESPACE__.'\DatabaseStatement')));
        $stmt->setFetchMode(PDO::FETCH_ASSOC);
        return $stmt;
    }

    /**
     * Prepares an executes an SQL statement with the parameters provided
     * @param string $sql
     * @param array $params
     */
    function execute($sql, $params = array()) {

        if($this->debug):
            var_dump("Statement:\n".$sql."\nParams: ".$this->fmt($params));
        endif;

        try {
            $stmt = $this->prepare($sql);
            $val = $stmt->execute((array) $params);
            if($stmt->errorCode() != '00000') error_log($this->errormsg());
            if($this->debug && $stmt->errorCode() != '00000'){
                var_dump($stmt->errorInfo());
                Errors::add("Database error: ".$this->errormsg(), E_USER_ERROR);
            }
            if(!$val) return false;
        } catch (PDOException $e){
            if($this->debug) var_dump($stmt->errorInfo());
            error_log($this->errormsg());
            Errors::add("Database error: ".$this->errormsg(), E_USER_ERROR);
            if($this->nested_transactions) $this->failTrans();
            else throw $e;
        }

        $this->stmt = $stmt;

        return $stmt;

    }    

    /**
     * Returns the value of the first column of the first row
     * of the database result.
     * @param $sql
     * @param $params
     */
    function getOne($sql, $params = array()){
        $stmt = $this->execute($sql, $params);
        return $stmt ? $stmt->getOne() : false;
    }

    /**
     * Fetches a single column (the first column) of a result set
     * @param $sql
     * @param $params
     */
    function getCol($sql, $params = array()){
        $stmt = $this->execute($sql, $params);
        return $stmt ? $stmt->getCol() : false;
    }

    /**
     * Fetches rows in associative array format
     * @param $sql
     * @param $params
     */
    function getAssoc($sql, $params = array()){
        $stmt = $this->execute($sql, $params);
        return $stmt ? $stmt->getAssoc() : false;
    }

    /**
     * Fetches rows in array format with columns
     * indexed by ordinal position
     * @param $sql
     * @param $params
     */
    function getArray($sql, $params = array()){
        $stmt = $this->execute($sql, $params);
        return $stmt ? $stmt->getArray() : false;
    }

    /**
     * Fetches all rows in associative array format
     * @param $sql
     * @param $params
     */
    function getAll($sql, $params = array()){
        return $this->getAssoc($sql, $params);
    }

    /**
     * Fetches rows in array format where the first column
     * is the key name and all other columns are values
     * @param $sql
     * @param $params
     */
    function getKeyPair($sql, $params = array()){
        $stmt = $this->execute($sql, $params);
        return $stmt ? $stmt->getKeyPair() : false;
    }

    /**
     * Fetches rows in multi-dimensional format where the first
     * column is the key name and all other colums are grouped
     * into associative arrays for each row
     * @param $sql
     * @param $params
     */
    function getGroup($sql, $params = array()){
        $stmt = $this->execute($sql, $params);
        return $stmt ? $stmt->getGroup() : false;
    }

    /**
     * Fetches only the first row and returns it as an
     * associative array
     * @param $sql
     * @param $params
     */
    function getRow($sql, $params = array()){
        $stmt = $this->execute($sql, $params);
        return $stmt ? $stmt->getRow() : false;
    }

    /**
     * Internal function used for formatting parameters in debug output
     * @param unknown_type $params
     */
    private function fmt($params){
        $arr = array();
        foreach((array) $params as $k=>$v){
            if(is_null($v)) $v = "NULL";
            elseif(is_bool($v)) $v = $v ? "TRUE" : "FALSE";
            $arr[] = "[".$k."] => ".$v;
        }
        return "Array(".join(", ", $arr).")";
    }

    /**
     * Returns the number of affected rows from an executed statement
     */
    function affected_rows(){
        return $this->stmt ? $this->stmt->rowcount() : false;
    }

    /**
     * Automated statement processing
     *
     * Params array takes the following fields:
     *
     *  - table         The name of the table to run the query on
     *
     *  - data          A key-value paired array of table data
     *
     *  - mode          INSERT, UPDATE, REPLACE, or NEW
     *
     *  - where         Can be a string or key-value set. Not used on INSERTs
     *                  If key-value set and numerically indexed, uses values from data
     *                  If key-value and keys are named, uses its own values
     *
     *  - params        An array of param values for the where clause
     *
     *  - returning     Optional string defining what to return from query.
     *                  Uses PostgreSQL's RETURNING construct
     *
     *  This method will return either a boolean indicating success, an array
     *  containing the data requested by returning, or a boolean FALSE indicating
     *  a failed query.
     *
     */
    function autoExecute($table, $params, $data){

        $fields = array(); //Temp array for field names
        $values = array(); //Temp array for field values
        $set = array(); //Temp array for update sets
        $ins = array(); //Insert value arguments

        $params['table'] = $table;
        $params['data'] = $data;

        $params['params'] = (array) $params['params'];

        //Parse the data set and prepare it for different query types
        foreach((array) $params['data'] as $field => $val):

            $fields[] = $field;
            $values[] = $val;
            $ins[] = "?";
            $set[] = $field . " = ?";

        endforeach;

        //Check for and convert the array/object version of the where clause param
        if(is_object($params['where']) || is_array($params['where'])):

            $clause = array();
            $params['params'] = array(); //Reset the parameters list

            foreach($params['where'] as $key => $val):

                if(is_numeric($key)):
                    //Numerically indexed elements use their values as field names
                    //and values from the data array as param values
                    $field = $val;
                    $params['params'][] = $params['data'][$val];
                else:
                    //Named elements use their own names and values
                    $field = $key;
                    $params['params'][] = $val;
                endif;

                $clause[] = $field . " = ?";

            endforeach;

            $params['where'] = join(" AND ", $clause);

        endif;

        //Figure out what type of query we want to run
        $mode = strtoupper($params['mode']);
        switch($mode):
            case 'NEW':
            case 'INSERT':

                //Build the insert query
                if(count($fields)):
                    $sql =  "INSERT INTO " . $params['table']
                            . " (" . join(", ", $fields) . ")"
                            . " SELECT " . join(", ", $ins);
                else:
                    $sql =  "INSERT INTO " . $params['table']
                            . " DEFAULT VALUES";
                endif;

                //Do we need to add a conditional check?
                if($mode == "NEW" && count($fields)):
                    $sql .= " WHERE NOT EXISTS ("
                            . " SELECT 1 FROM " . $params['table']
                            . " WHERE " . $params['where']
                            . " )";
                    //Add in where clause params
                    $values = array_merge($values, $params['params']);
                endif;

                //Do we need to add a returning clause?
                if($params['returning']):
                    $sql .= " RETURNING " . $params['returning'];
                endif;

                //Execute our query
                $result = $this->getRow($sql, $values);

                //Return our result
                if($params['returning']):
                    return $result;
                else:
                    return $result !== false;
                endif;

                break;
            case 'UPDATE':

                if(!count($fields)) return false;

                //Build the update query
                $sql =  "UPDATE " . $params['table']
                        . " SET " . join(", ", $set)
                        . " WHERE " . $params['where'];

                //Do we need to add a returning clause?
                if($params['returning']):
                    $sql .= " RETURNING " . $params['returning'];
                endif;

                //Add in where clause params
                $values = array_merge($values, $params['params']);

                //Execute our query
                $result = $this->getRow($sql, $values);

                //Return our result
                if($params['returning']):
                    return $result;
                else:
                    return $result !== false;
                endif;

                break;
            case 'REPLACE': //UPDATE or INSERT

                //Attempt an UPDATE
                $params['mode'] = "UPDATE";
                $result = $this->autoExecute($params['table'], $params, $params['data']);

                //Attempt an INSERT if UPDATE didn't match anything
                if($this->affected_rows() === 0):
                    $params['mode'] = "INSERT";
                    $result = $this->autoExecute($params['table'], $params, $params['data']);
                endif;

                return $result;

                break;
            case 'DELETE':

                //Don't run if we don't have a where clause
                if(!$params['where']) return false;

                //Build the delete query
                $sql =  "DELETE FROM " . $params['table']
                        . " WHERE " . $params['where'];

                //Do we need to add a returning clause?
                if($params['returning']):
                    $sql .= " RETURNING " . $params['returning'];
                endif;

                //Execute our query
                $result = $this->getRow($sql, $params['params']);

                //Return our result
                if($params['returning']):
                    return $result;
                else:
                    return $result !== false;
                endif;

                break;
            default:
                user_error('AutoExecute called incorrectly', E_USER_ERROR);
                break;
        endswitch;

    }

    /**
     * @see $this->startTrans()
     */
    function beginTransaction(){
        $this->startTrans();
    }

    /**
     * Starts a smart transaction handler. Transaction nesting is emulated
     * by this class.
     */
    function startTrans(){

        $this->nested_transactions++;
        if($this->debug) var_dump("Starting transaction. Nesting level: " . $this->nested_transactions);

        //Do we need to begin an actual transaction?
        if($this->nested_transactions === 1):
            parent::beginTransaction();
            $this->good_trans = true;
            $this->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
        endif;

    }

    /**
     * Returns TRUE if the transaction will attempt to commit, and
     * FALSE if the transaction will be rolled back upon completion.
     */
    function isGoodTrans(){
        return $this->good_trans;
    }

    /**
     * Marks a transaction as a failure. Transaction will be rolled back
     * upon completion.
     */
    function failTrans(){
        if($this->nested_transactions) $this->good_trans = false;
        if($this->debug):
            Errors::add("Database transaction failed: ".$this->errorMsg());
        endif;
        $this->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT);
    }

    /**
     * @see $this->rollbackTrans()
     */
    function rollback(){
        $this->rollbackTrans();
    }

    /**
     * Rolls back the entire transaction and completes the current nested
     * transaction. If there are no more nested transactions, an actual
     * rollback is issued to the database.
     */
    function rollbackTrans(){
        if($this->nested_transactions):
            $this->nested_transactions--;
            if($this->debug) var_dump("Rollback requested. New nesting level: " . $this->nested_transactions);
            $this->good_trans = false;
            if($this->nested_transactions === 0):
                $this->good_trans = null;
                $this->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT);
                if($this->debug) var_dump("Transaction rolled back.");
                parent::rollback();
                $this->dispatch_event('on_rollback');
            endif;
        endif;
    }

    /**
     * Clears the nested transactions stack and issues a rollback to the database.
     */
    function fullRollback(){
        while($this->nested_transactions) $this->rollbackTrans();
    }

    /**
     * Returns the number of nested transactions:
     * 0 - There is no transaction in progress
     * 1 - There is one transaction pending
     * >1 - There are nested transactions in progress
     */
    function pending_trans(){
        return $this->nested_transactions;
    }

    /**
     * @see $this->completeTrans()
     */
    function commit($fail_on_user_errors = false){
        return $this->completeTrans($fail_on_user_errors);
    }

    /**
     * Completes the current transaction and issues a commit or rollback to the database
     * if there are no more nested transactions. If $fail_on_user_errors is set, the
     * transaction will automatically fail if any errors are queued in the Errors class.
     * @param boolean $fail_on_user_errors
     */
    function completeTrans($fail_on_user_errors = false){

        if(!$this->nested_transactions) return;

        //Fail the transaction if we have user errors in the queue
        if($fail_on_user_errors && Errors::exist()) $this->good_trans = false;

        //Do we actually need to attempt to commit the transaction?
        if($this->nested_transactions === 1):

            if(!$this->good_trans || !parent::commit()){
                if($this->debug) var_dump("Transaction failed: " . $this->errormsg());
                $this->rollbackTrans();
                return false;
            }

            //Transaction was good
            $this->nested_transactions--;
            $this->good_trans = null;
            $this->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT);
            if($this->debug) var_dump("Transaction committed.");
            $this->dispatch_event('on_commit', false);
            return true;
        else:
            //Don't take action just yet as we are still nested
            $this->nested_transactions--;
            if($this->debug) var_dump("Virtual commit. New nesting level: " . $this->nested_transactions);
        endif;

        return $this->good_trans;

    }

    /**
     * Returns the text of the most recently encountered error
     */
    function errormsg(){
        $msg = $this->errorInfo();
        return $msg[2];
    }

}

class DatabaseStatement extends \PDOStatement implements \Countable {

    /**
     * Binds passed parameters according to their PHP type and executes
     * the prepared statement
     */
    function execute($params = array()) {
        $i = 1;
        foreach($params as $k => $v):
            $mode = PDO::PARAM_STR;
            if(is_null($v)) $mode = PDO::PARAM_NULL;
            elseif(is_bool($v)) $mode = PDO::PARAM_BOOL;
            elseif(is_resource($v)) $mode = PDO::PARAM_LOB;
            $this->bindParam($i, $params[$k], $mode);
            $i++;
        endforeach;
        $ok = parent::execute();
        return $ok ? $this : false;
    }

    /**
     * Returns the value of the first column of the first row
     */
    function getOne() {
        return $this->fetchColumn(0);
    }

    /**
     * Returns an array of values of the column found at $index
     * position.
     * @param $index
     */
    function getCol($index=0) {
        return $this->fetchAll(PDO::FETCH_COLUMN, $index);
    }

    /**
     * Returns all rows in numeric array format
     */
    function getArray(){
        return $this->fetchAll(PDO::FETCH_NUM);
    }

    /*
     * Returns all rows in associative array format
     */
    function getAll(){
        return $this->fetchAll(PDO::FETCH_ASSOC);
    }

    /**
     * Returns all rows in associative array format
     */
    function getAssoc() {
        return $this->fetchAll(PDO::FETCH_ASSOC);
    }

    /**
     * Returns rows in multi-dimensional format where the first
     * column is the key name and all other colums are grouped
     * into associative arrays for each row
     */
    function getGroup() {
        return $this->fetchAll(PDO::FETCH_GROUP);
    }

    /**
     * Returns a single row in associative format
     */
    function getRow(){
        return $this->fetch(PDO::FETCH_ASSOC);
    }

    /**
     * Fetches rows in array format where the first column
     * is the key name and all other columns are values
     */
    function getKeyPair(){
        //Emulate it
        $tmp = $this->fetchAll(PDO::FETCH_ASSOC);
        $arr = array();
        for($i = 0; $i < count($tmp); $i++){
            $arr[array_shift($tmp[$i])] = count($tmp[$i]) > 1 ? $tmp[$i] : array_shift($tmp[$i]);
        }
        return $arr;
    }

    /**
     * Returns the number of rows returned by this statement
     */
    function recordCount(){

        return $this->rowCount();

    }

    /**
     * Returns the number of rows returned by this statement
     */
    function count(){

        return $this->rowCount();

    }
}
Kenaniah
+1  A: 

Even though my DB class does not use prepared statements I still want to mention it here. I see no reason at all to implement everything with prepared statements. I do know that prepared statements are faster, but only when used multiple times. If you execute the query only once (and this is the only type of query I normally need to use), it is slower. Thus it is counterproductive to use prepared statements everywhere.

Proper description of the class may be found some place else at stackoverflow. But here some of the good stuff:

  • Less then 100 lines database layer
  • DB::x for DB::instance()->execute and DB::q for DB::instance()->query
  • autoQuoting with two types of placeholders ? and ?x (where x may be ,, & and |). The ?, placeholder may be used as an UPDATE helper here.

But for full information see the stackoverflow post linked above ;)

PS: The README in the repo does not apply to this class. It is for the normal DB.php, not for DB_intelligent.php. PPS: The class is written for PHP 5.3. If you want to use it on PHP 5.2 simply copy all those PDO methods from DB_forPHP52.php to DB_intelligent.php and remove the __callStatic method.

nikic
Yeah I think the same about prepared statements, and I am happy with my own database class/helpers too. But decided to try out PDO. Thanks for your class, it's very interesting approach, I am going to investigate it throughly. It's good food for thoughts for the next step I am going to take. The only thing I am trying to keep in mind is to keep these classes/helpers as easy as possible, to make it possible to use it as answers here on SO. I think of complete answers, opposite to usual "use prepared statements" ones. To give a reliable tool as an answer, not a sketch. But understandable one.
Col. Shrapnel
+1  A: 

Just in addition to other answers: a method for proper quote of column names:

/**
 * Escape identifier (database/table/column name) - ie. if you're using MySQL:
 * db_name.tbl_name.col_name -> `db_name`.`tbl_name`.`col_name`
 **/
protected function quoteIdentifier($identifier) {
    static $escapeChars = array(
        'mysql'  => '``',
        'oracle' => '""',
        'mssql'  => '[]',
        //...
    );

    $escape = $escapeChars[$this->getAttribute(self::ATTR_DRIVER_NAME)];
    $identifier = (array) explode('.', $identifier);
    $identifier = array_map(function($segment) use($escape) {
        return $escape[0] . $segment . $escape[1];
    }, $identifier);

    return implode('.', $identifier);
}
Crozin
This looks interesting. But I think that this isn't totally correct. For example if MySQL runs in ANSI mode you use `"` for field names (though I don't know, whether ``` is supported too). I think it is very sad that PDO doesn't support escaping field names natively.
nikic
Seems like MySQL still allows ``` in `ANSI_QUOTES` mode. There already is a [feature request](http://bugs.php.net/bug.php?id=38196) to implement this in PDO - but it's four years old...
nikic
Thanks, good point. Though I am not going to implement any database compatibility. It's a long way to go and I doubt I will ever need it.
Col. Shrapnel
A: 

Thanks to everyone.
Every answer was helpful and I wish I could split the bounty.

At the end, to my surprise, I was able to make it the same way as before, based on on accepted answer

$fields = array("login","password");
$_POST['password'] = MD5($_POST['login'].$_POST['password']);
$stmt = $dbh->prepare("UPDATE users SET ".pdoSet($fields,$values)." WHERE id = :id");
$values["id"] = $_POST['id'];
$stmt->execute($values);

It can be wrapped into a helper function, but I doubt there is necessity. It will shorten the code by just one line.

pdoSet code:

function pdoSet($fields, &$values, $source = array()) {
  $set = '';
  $values = array();
  if (!$source) $source = &$_POST;
  foreach ($fields as $field) {
    if (isset($source[$field])) {
      $set.="`$field`=:$field, ";
      $values[$field] = $source[$field];
    }
  }
  return substr($set, 0, -2); 
}
Col. Shrapnel
+1 for the post!
abel