Search code examples
phporaclephp-7oracle-call-interfaceoci8

php7, references and oci_bind_by_name


I'm posting this here before php.net to maybe get a better understanding of the difference in behavior that I'm seeing between PHP 5.x and 7.x.

The following code works in PHP 5.x but not 7.x

$conn = oci_connect('****', '****', '****', '****');
$stmt = oci_parse($conn, 'select record# from company where record#=:1');

$cache = [];

$cacheRow[0] = '2270';

oci_bind_by_name($stmt, ":1", $cacheRow[0], 2*strlen($cacheRow[0])+32);

$cache[0] = $cacheRow;

$result = runStmt($stmt);
checkResult($result, '2270');

$cacheRow = $cache[0];
$cacheRow[0] = '2274';
$cache[0] = $cacheRow;

$result = runStmt($stmt);
checkResult($result, '2274');

runStmt() just oci_execute and oci_fetch_array. checkResult() just verifies that the row returned contains the value in the second parameter.

In PHP 7 (7.0.8 and 7.0.10 anyway) the second call to checkResult reports that the row returned contains the RECORD# 2270 not the expected 2274.

Tracing through the code in gdb here's what I've pieced together:

  • The &$variable parameter of oci_bind_by_name ends up be dereferenced by z/ and lives on as a simple string zval in bindp->zval (oci8_statement.c:1250). This is ok, as other simpler tests work as long as all the zvals are pointing at the same string.

  • On return from oci_bind_by_name $cacheRow[0] is now a reference as expected.

  • On the next $cacheRow[0] = '2274' when the copy of $cacheRow is made during the assignment, $cacheRow[0] in the resulting copy is no longer a reference, just a zval pointing to the original string.

  • After the copy when the assignment into the new $cacheRow[0] is made it just changes its str pointer.

Now the new $cacheRow[0] is pointing to a different string than oci8_statement's bindp->zval so the next oci_execute will pull the old bound value.

I can work around this by ensuring that the assignments involving $cache[0] (both in-to and out-of) are by-reference. This avoids the issue because the $cacheRow array is never separated.

I can also reproduce this in pure PHP code

function bbn1(&$var)
{
}

function test1()
{
    $cache = [];

    $cacheRow[0] = '2270';

    bbn1($cacheRow[0]);
    $x = $cacheRow[0];

    $cache[0] = $cacheRow;

    $cacheRow = $cache[0];
    // Copy-on-write of $cacheRow does not preserve the reference in 
    // $cacheRow[0] because $cacheRow[0]'s refcount == 1
    // zend_array_dup_element in zend_hash.c
    $cacheRow[0] = '2274';

}

function bbn2(&$var)
{
    static $cache = [];
    $cache[] =& $var;
}

function test2()
{
    $cache = [];

    $cacheRow[0] = '2270';

    bbn2($cacheRow[0]);
    $x = $cacheRow[0];

    $cache[0] = $cacheRow;

    $cacheRow = $cache[0];

    // Copy-on-write of $cacheRow preserves the reference in 
    // $cacheRow[0] because $cacheRow[0]'s refcount != 1
    // zend_array_dup_element in zend_hash.c
    $cacheRow[0] = '2274';

}

Since I can get different behaviors in the pure PHP tests depending on if I keep a reference to the passed parameter to bbn this makes me think that if oci_bind_by_name increased the refcount on its incoming bind_var parameter my original test would behave identically between PHP 5 and PHP 7. Then again, I'm willing to be convinced that this is expected behavior and I really do need to use assignment-by-ref.


Solution

  • Try the PHP OCI8 patch just uploaded to https://bugs.php.net/patch-display.php?bug_id=71148&patch=oci8-php7-bind&revision=latest