How to use PHP and PEAR MDB2 (Tutorial)

While writing some PHP Training materials for Pale Purple, I thought I'd add an updated guide on PHP and database access. I've already done one on PEAR::DB, but PEAR::MDB2 is it's successor and has a slightly different API.... and as PEAR::DB is now deprecated, it's probably about time I rewrote it anyway.

What is PEAR::MDB2?

MDB2 is yet another database API for PHP. It's written in PHP, so can be used in a cross platform manner. Because it's written in PHP, it's not going to be as 'fast' as the PDO library, however it's portability may make up for that.

Compared to the native bundled legacy PHP libraries (mysqli_*, pgsql_*, sqlite_*, mysql_), changing which database you are using can be as simple as changing the connection parameters to your database. The PDO libraries offer the same advantage, however they require PHP5.

Getting started

It's recommended you install it using the Pear installer, like the following :

pear install MDB2
pear install MDB2_Driver_$db

Where $db would be one of e.g. mysql, pgsql, sqlite, ori8, mssql etc.

You'll note, that unlike PEAR::DB, it's necessary to download a specific driver for each database you wish to support.

Connecting to a database

One of the advantages of MDB2 over PEAR::DB is that it offers a number of different ways of providing a DB connection - either lazy (MDB2::factory()), optimistic (MDB2::connect()) or singleton (MDB2::singleton()) . I personally tend to just go with the lazy 'factory' method where the connection isn't created until it's used.

As with PEAR::DB, you specify which database to connect to using a JDBC style URL, which looks something like : $db_driver://$user:$password@$host/$database_name, so for example pgsql://web:password@dbserver/my_database would be a valid url.

<?php 
require_once("MDB2.php");
$con = MDB2::factory($url);

Error checking

As always, there is plenty of scope for something to go wrong. Perhaps the database server is offline, or your username/password are incorrect. In any case, error handling with MDB2 follows the PEAR convention of using PEAR::isError($return_value). So in our case, our code could now look a bit like :

<?php
require_once("MDB2.php");
$con = MDB2::factory($url);
if(PEAR::isError($con)) {
    die("Error while connecting : " . $con->getMessage());
}
// use $con

Security (SQL Injection)

When ever talking to a relational database, care has to be taken that any parameters passed into a query have been appropriately sanitised. Failure to sanitise data will make your site vulnerable to SQL injection, data loss or compromise. If you don't know what SQL Injection is, try reading the following article at lwn.net.

Issuing Queries on the database

Select queries (returning data)

<?php
// load library as above.
// connect to db as above. ($con = ... )
// check $con validity as per above example.

$sql = "SELECT * FROM the_table";
$resultset = $con->query($sql);
if(PEAR::isError($resultset)) {
    die('Failed to issue query, error message : ' . $resultset->getMessage());
}

while($row = $resultset->fetchRow(MDB2_FETCHMODE_ASSOC)) {
    foreach($row as $field => $value) {
        echo "$field / $value \n";
    }
}

Non returning queries (update, insert, delete)

This is largely the same as above, apart from the fact there is no need to do the while loop / fetchRow section, so the code becomes somewhat simpler and would look a bit like :

// load library
// connect to db, and check for errors
$sql = "UPDATE table SET column = 5 WHERE id = 1";
$result = $con->query($sql);
if(PEAR::isError($result)) {
    die("Failed to issue query, error message : " . $result->getMessage();
}

If you need to add in user supplied data into the query, you can use $save_var = $con->quote($var). An optional second parameter can be given to determine whether the data should be reformatted as a boolean, date, integer or text.

Prepared statements (or how to safely pass parameters into a database query)

A safer (and potentially better performing) approach to insert user supplied data into queries is to use Prepared Statements. These can be faster, as if the underlying database supports it, the statement itself can be compiled and cached, so saving the database engine from having to reparse the same SQL each time. They are safer, because SQL injection is not possible when using them - because the database knows what goes where. Some database engines (e.g. MySQL v4) do not support prepared statements, in which case, like PEAR::DB, PEAR::MDB2 will emulate the functionality in the background.

A prepared statement example:

<?php
// load library, get connection etc (as per above)

$sql = "UPDATE table SET column = ? WHERE id = ?";
$types = array('integer', 'text');
$statement = $con->prepare($sql, $types, MDB2_PREPARE_MANIP);
$data = array(5, 'blah');
$affected_rows = $statment->execute($data);
if(PEAR::isError($affected_rows)) {
    // die etc.
}

MDB2_PREPARE_MANIP is required if you wish to manipulate data, and not return any values. MDB2_PREPARE_RESULT is used if you wish to run a prepared statement and have data returned, for instance :

The $types array is not required (you could replace it with TRUE), as MDB2 can automatically handle it.

<?php
// load library, get db connection etc.
$types = array("text");
$statement = $con->prepare("SELECT * FROM table WHERE column = ?", $types, MDB2_PREPARE_RESULT);
$data = array('foo');
$resultset = $statement->execute($data);
if(PEAR::isError($resultset)) {
    die('failed... ' . $resultset->getMessage());
}
$statement->Free();
while($row = $resultset->fetchRow(MDB2_FETCHMODE_ASSOC)) {
    echo "Found : " . $row['column'] . "\n";
}

Suggestions for Usage

I'm not particularly keen on writing the same thing twice, so I tend to use functions like the following :

<?php
require_once("MDB2.php");
function connect() {
    $con = MDB2::factory($url);
    if(PEAR::isError($con)) {
        die("Error while connecting : " . $con->getMessage());
    }
    return $con;
}

Then something like the following to undertake queries :

function execute_query($sql, $values=array()) {
    $con = connect();
    $results = array();
    if(sizeof($values) > 0) {
        $statement = $con->prepare($sql, TRUE, MDB2_PREPARE_RESULT);
        $resultset = $statement->execute($values);
        $statement->free();
    }
    else {
        $resultset = $con->query($sql);
    }
    if(PEAR::isError($resultset)) {
        die('DB Error... ' . $resultset->getMessage());
    }

    while($row = $resultset->fetchRow(MDB2_FETCHMODE_ASSOC)) {
        $results[] = $row;
    }
    return $results;
}

Thanks for reading; feel free to leave comments/suggestions (or questions).

PDO vrs MDB2

I think it's worth reiterating that comparing PDO to MDB2 is like apples to oranges.

PDO provides a data-access abstraction layer, which means that, regardless of which database you're using, you use the same functions to issue queries and fetch data. PDO does not provide a database abstraction; it doesn't rewrite SQL or emulate missing features. You should use a full-blown abstraction layer if you need that facility.

http://uk.php.net/pdo

Too many people that I've worked with think that PDO == PEAR::DB/MDB2, but written as a C extension.

PDO

PDO shouldnt really be compared on the same level as MDB2, DB , etc. It is not a full fledged abstraction layer. It is a data-access package. For PDO you have to write db spefic SQL. With MDB2 and DB you use their packages lang. and that can be moved from db to db without having to rewrite the SQL as you would if you used PDO.... OR at least that is my understanding.

I have been wanting to move to using PDO, and have no real idea when I would need a different DB, but it just seems that if you are writing it, why no make it as portable as possible?

And since mysqli makes use of bound parameters (and pdo using db specific sql anyway), I still cant figure out what the benefit of using PDO is?

If you can provide any feedback that would be great.

Thanks.

PDO

Yes.. only PDO, nothing else. Check also propel.

Propel ++ :-)

I agree; Propel rocks... I think I've written about it somewhere here....

http://codepoets.co.uk/propel_php5_framework_quickstart_howto_guide

MDB2 and Locking

Thanks for the tutorial.

Any idea how to implement table locking with MDB2? I know it's supported by the latest stable version but can't find any documentation...

prepared statements confusion..

I'm a total newbie to php, coming from a coldfusion background. One thing that was a bit confusing for me in the prepared statement example was the position of the array... typically ID's are intergers and not text. It took me a while to figure out which ? went where in the $sql= line...

well spotted

Yes, normally a field called 'id' would be an int...

Array oopsy-daisy

Hey, thanks for this quickstart to PEAR'S MDB2. I'm finding it very useful. One thing I noticed, though: in the execute_query function, line 16, the brackets in $results[] = $row put the entire $row array in the first entry of $results. If you drop the brackets, then it just copies the entire array, so then you don't have the awkward array within array ($results[][]). And would you do an isError on $resultset in the else statement in the same function?

isError on $resultset

Error wise - yes; well spotted. I've changed the code above to include this.

thanks
David.

Re: Array oopsy-daisy

I caught my mistake. If there are more than one rows matching the query, then the while will go more than once and so you need the array inside an array. Whoops.

Yes, indeed :)

It's a very simple API for database use, hence you have a list which may be empty, contain one item, or many - and each item is a row in the database.

Good luck :)

this looks not so much OOP to me?

so, just curious - why would you leave all these functions (connect(), execute_query()) laying around? Wouldn't it make more sense to extend the MDB2 class and encapsulate the functions in your new class? This way, the connection info can be a private variable, you can prevent stuff from happening to the db without your db object knowing about it, and everything is a lot cleaner conceptually? Not being a troll - I'm new to php and honestly curious: what's your thought process or guiding concept in laying the code out this way?

that's because it's not really OO...

You're quite right; when writing the code OOP wasn't really on our mind(s) - it is just a method of illustrating some basic encapsulation and how functions can be used for code reuse.

Yes, you could take the view that it would make more sense to extend the MDB2 class, but on that thought it might be better to not extend the MDB2 class, but instead write a new object that uses the MDB2 library - with the intention of it being a generic DB class which you can modify in the future allowing you to change the DB library in use without the 'end user' being aware... for example you could then potentially change from MDB2 to PDO...

ahh...

thx for the followup. It's terribly difficult to find real OOP examples using PHP - perhaps because authors can only expect very, very little of 'the average php programmer'. I guess a real example involving paranoid exception-handling, intelligent use of >1 pear package, etc., might not make any sense to its intended audience. Maybe I"ll just switch to writing web pages in lisp ;)

Post new comment

The content of this field is kept private and will not be shown publicly.
  • Allowed HTML tags: <a> <em> <strong> <cite> <code> <ul> <ol> <li> <dl> <dt> <dd> <img>
  • Lines and paragraphs break automatically.

More information about formatting options

CAPTCHA
This question is used to make sure you are a human visitor and to prevent spam submissions.
7 + 2 =
Solve this simple math problem and enter the result. E.g. for 1+3, enter 4.