22
votes

I'm having a strange issue with pdo_odbc and PDO::FETCH_OBJ (and PDO::FETCH_CLASS) that results in the following error message:

PHP Fatal error:  Cannot access empty property

Here's the code:

$dbh = new PDO("odbc:FOO");

$sth = $dbh->query("
  SELECT rolename
  FROM dbc.allrolerights
  WHERE databasename = 'BAR'
");

$result = $sth->fetch(PDO::FETCH_OBJ);

The FOO DSN, for reference, is a Teradata datasource using the tdata.so driver, provided by the tdodbc package.

I believe this is happening because the field name (as returned from the ODBC query) is blank when PDO calls zend_API.h:object_init_ex() to instantiate the stdClass object. If I switch to PDO::FETCH_LAZY and var_dump() the row, I get the following:

object(PDORow)#3 (2) {
  ["queryString"]=>
  string(95) "
  SELECT rolename
  FROM dbc.allrolerights
  WHERE databasename = 'BAR'
"
  [""]=>
  string(30) "FNAR                          "
}

Which seems to back it up. I've tried several different PDO attribute combinations and bunch of different angles to work around the problem. One solution is to fetch an associative array and pass it to the class constructor. However, this doesn't work for certain frameworks and ORMs that are using PDO::FETCH_CLASS directly, behind the scenes.

I want to add that other fetch methods seem to do the right thing, for example, PDO::FETCH_NAMED:

array(1) {
  ["RoleName"]=>
  string(30) "FNAR                          "
}

I'm looking for something I can put in the PDO dbh or sth definition, or in the odbc.ini or odbcinst.ini for the datasource or driver, to resolve this issue. Thank you in advance.

Update: odbc_fetch_object() (i.e. not PDO) works great with the same exact everything. Just wanted to mention it. There clearly doesn't seem to be any serious issues with PHP, unixODBC, or the ODBC driver. It's something in the PDO code. Time to open a bug report... opened

$dbh = odbc_connect("FOO", NULL, NULL)
  or die(odbc_error_msg());

$sth = odbc_exec($dbh, "
  SELECT rolename
  FROM dbc.allrolerights
  WHERE databasename = 'BAR'
");

$result = odbc_fetch_object($sth);
var_dump($result);

And the output:

object(stdClass)#1 (1) {
  ["RoleName"]=>
  string(30) "FNAR                          "
}

Update 2: The situation continues to grow more and more bizarre. I can do a PDO::FETCH_LAZY and see the blank column name as seen in the var_dump() above, but if I try to access the property by name (e.g. $result->RoleName), it works! What are these fetch methods doing so differently that some of them can sometimes access the field names, and others cannot?

Side-by-side comparisons of ODBC traces ("working" cf. "not working") shows no differences (other than different pointer addresses). PDO::FETCH_BOUND works with both numbered and named columns. PDO::FETCH_INTO an object with a RoleName property does not.

2
Try to upgrade PHP first.Your Common Sense
I tried 5.5.9. No change.mwp
There are two thing i can suggest: your SELECT rolename isn't case sensitive is it? PDO might get a bit funny about that. Also if you join onto a table with the same column the column might not come back at all unless you select dup as dup1 in the SQL. Also one final suggestion try backticking '`' the column name..Kevin Andrews
I played around with different cases and even tried setting PDO::ATTR_CASE to force different behaviors, with no change. This is a simple select without a join so no duplicate columns; aliasing the column doesn't change anything. I had not tried backticks, but those won't work with Teradata. I used double quotes, which seems to be the correct method of quoting reserved words in Teradata, and there was no change. Thank you!mwp

2 Answers

3
votes

Your question describes two problems:

  1. Why is it that objects cannot be created with properties having empty-string names when using PDO::FETCH_OBJ, but apparently can when using other methods?

    As documented under Internal structures and implmentation in the PHP Internals Book, "dynamic properties" (i.e. an object's member variables that are not declared in its class definition, but rather are created at runtime) are implemented as a hash table.

    If one wishes to populate a newly instantiated standard object with a bunch of properties that are currently held in a hash table, one can simply point the object's properties variable at that existing hash table—PHP's object_and_properties_init() function, which is called by odbc_fetch_object(), does exactly that without performing any sanity checking on the table's keys. Consequently, one can instantiate an object with strange property names (such as the empty string).

    On the other hand, if one already has an instantiated object and needs to set a property value (whilst preserving any others that already exist), one must copy that value into the object's hash table—PHP's zend_std_write_property() method, which handles this action for standard objects, does exactly that having first performed a sanity check on the property name. Consequently, one cannot add a property with a strange name (such as the empty string) to an existing object.

    This discrepancy in sanity-checking between the two approaches appears, to my mind, to be a bug: any restrictions on dynamic property names should be enforced irrespective of the method by which such properties are created. Whether strange names of this sort should be allowed (and therefore the sanity checking should be removed from the latter method), or disallowed (and therefore some sanity checking should be added to the former method), is a decision that I shall leave to the PHP developers.

    How does PDO fit into all this?

    PDOStatement::fetch() first prepares the destination into which results will be stored and then iterates over the columns storing each field in turn: I imagine it does this in order to simplify the codebase, as each fetch style can be implemented within the same structure. However, this does mean that when called using the PDO::FETCH_OBJ style (and also both PDO::FETCH_CLASS and PDO::FETCH_INTO, as you have observed), an object is instantiated first and its properties are populated later. Consequently, strange property names (such as the empty string) result in the observed failure.

    The other fetch styles that you have tried don't experience the same problem because:

    • PDO::FETCH_BOUND fetches into variables that were specified by a previous call to PDOStatement::bindColumn(), so PHP never attempts to write to a property having an empty name;

    • PDO::FETCH_LAZY skips the whole shebang and does things similar manner to odbc_fetch_object() above.

    Similarly, array-based fetch styles won't suffer similar problems because empty-string keys are perfectly valid in those hash tables.

  2. Why is it that PDO thinks the column names in this ODBC recordset are empty strings?

    The answer to this is much less obvious to me.

    We saw earlier that, to populate properties, PDO uses stmt->columns[i].name as the property name. This should have been correctly filled at an earlier point, when pdo_stmt_describe_columns() was called. This function in turn had called the driver's describer method for each column in the resultset: in the case of PDO_ODBC, that's odbc_stmt_describe() which does indeed assign a value to that field.

    So, everything looks fine on the PHP side. It would be interesting to know whether the call to the driver's SQLDescribeCol() function correctly populated the column name into the buffer provided as its third argument: one imagines not, which would suggest that the problem lies in the ODBC driver itself. You mentioned that you're using Teradata: but are you sure that you are using the same driver for both PDO_ODBC (which isn't working) and ext/odbc (which is)?

    In particular, Teradata document under Extension Level Functions:

    By default, SQLDescribeCol and SQLColAttribute return the column name instead of the Teradata column title. If an application wants ODBC Driver for Teradata to return the column title instead of the actual column name, then the option Use Column Names in the Teradata ODBC Driver Options dialog box must not be selected for the DSN used, or set DontUseTitles = No on the UNIX OS.

    Returning the column title instead of the actual column name can cause problems for certain applications, such as Crystal Reports, because they expect to get the column name and not the column title.

-1
votes

I think a "solution" by now wold be use the fetch style PDO::FETCH_NAMED and then convert the returned array and populate dynamic class:

function arrayToObject(array $array){
   $obj = new stdClass();
   foreach($array as $k => $v)
      $obj->$k = $v;

   return $obj;
}