Skip to content
Merged
1 change: 1 addition & 0 deletions UPGRADING
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ PHP 8.5 UPGRADE NOTES
========================================

- Core:
. Closure is now a proper subtype of callable
. Added support for Closures in constant expressions.
RFC: https://wiki.php.net/rfc/closures_in_const_expr

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
--TEST--
Closure should be covariant with callable
--FILE--
<?php

class A {
public function foo(Closure $c): callable {}
}
class B extends A {
public function foo(callable $c): Closure {}
}
?>
OK
--EXPECT--
OK
14 changes: 14 additions & 0 deletions Zend/zend_inheritance.c
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
#include "zend_execute.h"
#include "zend_inheritance.h"
#include "zend_interfaces.h"
#include "zend_closures.h"
#include "zend_smart_str.h"
#include "zend_operators.h"
#include "zend_exceptions.h"
Expand Down Expand Up @@ -490,6 +491,19 @@ static inheritance_status zend_is_class_subtype_of_type(
}
}

/* If the parent has 'callable' as a return type, then Closure satisfies the co-variant check */
if (ZEND_TYPE_FULL_MASK(proto_type) & MAY_BE_CALLABLE) {
if (!fe_ce) fe_ce = lookup_class(fe_scope, fe_class_name);
if (!fe_ce) {
have_unresolved = 1;
} else if (fe_ce == zend_ce_closure) {
track_class_dependency(fe_ce, fe_class_name);
return INHERITANCE_SUCCESS;
} else {
return INHERITANCE_ERROR;
}
}

zend_type *single_type;

/* Traverse the list of parent types and check if the current child (FE)
Expand Down
32 changes: 20 additions & 12 deletions ext/pdo/pdo_stmt.c
Original file line number Diff line number Diff line change
Expand Up @@ -836,14 +836,14 @@ static bool do_fetch(pdo_stmt_t *stmt, zval *return_value, enum pdo_fetch_type h

case PDO_FETCH_INTO:
/* TODO: Make this an assertion and ensure this is true higher up? */
if (Z_ISUNDEF(stmt->fetch.into)) {
if (stmt->fetch.into == NULL) {
/* TODO ArgumentCountError? */
pdo_raise_impl_error(stmt->dbh, stmt, "HY000", "No fetch-into object specified.");
return 0;
break;
}

ZVAL_COPY(return_value, &stmt->fetch.into);
ZVAL_OBJ_COPY(return_value, stmt->fetch.into);

if (Z_OBJ_P(return_value)->ce == ZEND_STANDARD_CLASS_DEF_PTR) {
how = PDO_FETCH_OBJ;
Expand Down Expand Up @@ -1655,9 +1655,9 @@ bool pdo_stmt_setup_fetch_mode(pdo_stmt_t *stmt, zend_long mode, uint32_t mode_a

switch (stmt->default_fetch_type) {
case PDO_FETCH_INTO:
if (!Z_ISUNDEF(stmt->fetch.into)) {
zval_ptr_dtor(&stmt->fetch.into);
ZVAL_UNDEF(&stmt->fetch.into);
if (stmt->fetch.into) {
OBJ_RELEASE(stmt->fetch.into);
stmt->fetch.into = NULL;
}
break;
default:
Expand Down Expand Up @@ -1786,7 +1786,8 @@ bool pdo_stmt_setup_fetch_mode(pdo_stmt_t *stmt, zend_long mode, uint32_t mode_a
return false;
}

ZVAL_COPY(&stmt->fetch.into, &args[0]);
GC_ADDREF(Z_OBJ(args[0]));
stmt->fetch.into = Z_OBJ(args[0]);
break;
default:
zend_argument_value_error(mode_arg_num, "must be one of the PDO::FETCH_* constants");
Expand Down Expand Up @@ -2027,8 +2028,16 @@ static zend_function *dbstmt_method_get(zend_object **object_pp, zend_string *me
static HashTable *dbstmt_get_gc(zend_object *object, zval **gc_data, int *gc_count)
{
pdo_stmt_t *stmt = php_pdo_stmt_fetch_object(object);
*gc_data = &stmt->fetch.into;
*gc_count = 1;
enum pdo_fetch_type default_fetch_mode = stmt->default_fetch_type & ~PDO_FETCH_FLAGS;

zend_get_gc_buffer *gc_buffer = zend_get_gc_buffer_create();
zend_get_gc_buffer_add_zval(gc_buffer, &stmt->database_object_handle);
if (default_fetch_mode == PDO_FETCH_INTO) {
zend_get_gc_buffer_add_obj(gc_buffer, stmt->fetch.into);
} else if (default_fetch_mode == PDO_FETCH_CLASS) {
zend_get_gc_buffer_add_zval(gc_buffer, &stmt->fetch.cls.ctor_args);
}
zend_get_gc_buffer_use(gc_buffer, gc_data, gc_count);

/**
* If there are no dynamic properties and the default property is 1 (that is, there is only one property
Expand Down Expand Up @@ -2074,9 +2083,9 @@ PDO_API void php_pdo_free_statement(pdo_stmt_t *stmt)

pdo_stmt_reset_columns(stmt);

if (!Z_ISUNDEF(stmt->fetch.into) && stmt->default_fetch_type == PDO_FETCH_INTO) {
zval_ptr_dtor(&stmt->fetch.into);
ZVAL_UNDEF(&stmt->fetch.into);
if (stmt->fetch.into && stmt->default_fetch_type == PDO_FETCH_INTO) {
OBJ_RELEASE(stmt->fetch.into);
stmt->fetch.into = NULL;
}

do_fetch_opt_finish(stmt, 1);
Expand Down Expand Up @@ -2165,7 +2174,6 @@ static void pdo_stmt_iter_move_forwards(zend_object_iterator *iter)

if (!do_fetch(stmt, &I->fetch_ahead, PDO_FETCH_USE_DEFAULT,
PDO_FETCH_ORI_NEXT, /* offset */ 0, NULL)) {

PDO_HANDLE_STMT_ERR();
I->key = (zend_ulong)-1;
ZVAL_UNDEF(&I->fetch_ahead);
Expand Down
2 changes: 1 addition & 1 deletion ext/pdo/php_pdo_driver.h
Original file line number Diff line number Diff line change
Expand Up @@ -621,7 +621,7 @@ struct _pdo_stmt_t {
zval dummy; /* This exists due to alignment reasons with fetch.into and fetch.cls.ctor_args */
zend_fcall_info_cache fcc;
} func;
zval into;
zend_object *into;
} fetch;

/* used by the query parser for driver specific
Expand Down
138 changes: 138 additions & 0 deletions ext/pdo/tests/pdo_stmt_cyclic_references.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
--TEST--
PDO Common: Cyclic PDOStatement child class
--EXTENSIONS--
pdo
--SKIPIF--
<?php
$dir = getenv('REDIR_TEST_DIR');
if (false == $dir) die('skip no driver');
require_once $dir . 'pdo_test.inc';
PDOTest::skip();
?>
--FILE--
<?php
if (getenv('REDIR_TEST_DIR') === false) putenv('REDIR_TEST_DIR='.__DIR__ . '/../../pdo/tests/');
require_once getenv('REDIR_TEST_DIR') . 'pdo_test.inc';

class Ref {
public CyclicStatement $stmt;
}

class CyclicStatement extends PDOStatement {
protected function __construct(public Ref $ref) {}
}

class TestRow {
public $id;
public $val;
public $val2;

public function __construct(public string $arg) {}
}

$db = PDOTest::factory();
$db->exec('CREATE TABLE pdo_stmt_cyclic_ref(id INT NOT NULL PRIMARY KEY, val VARCHAR(10), val2 VARCHAR(10))');
$db->exec("INSERT INTO pdo_stmt_cyclic_ref VALUES(1, 'A', 'AA')");
$db->exec("INSERT INTO pdo_stmt_cyclic_ref VALUES(2, 'B', 'BB')");
$db->exec("INSERT INTO pdo_stmt_cyclic_ref VALUES(3, 'C', 'CC')");

$db->setAttribute(PDO::ATTR_STATEMENT_CLASS, ['CyclicStatement', [new Ref]]);

echo "Column fetch:\n";
$stmt = $db->query('SELECT id, val2, val FROM pdo_stmt_cyclic_ref');
$stmt->ref->stmt = $stmt;
$stmt->setFetchMode(PDO::FETCH_COLUMN, 2);
foreach($stmt as $obj) {
var_dump($obj);
}

echo "Class fetch:\n";
$stmt = $db->query('SELECT id, val2, val FROM pdo_stmt_cyclic_ref');
$stmt->ref->stmt = $stmt;
$stmt->setFetchMode(PDO::FETCH_CLASS, 'TestRow', ['Hello world']);
foreach($stmt as $obj) {
var_dump($obj);
}

echo "Fetch into:\n";
$stmt = $db->query('SELECT id, val2, val FROM pdo_stmt_cyclic_ref');
$stmt->ref->stmt = $stmt;
$stmt->setFetchMode(PDO::FETCH_INTO, new TestRow('I am being fetch into'));
foreach($stmt as $obj) {
var_dump($obj);
}

?>
--CLEAN--
<?php
require_once getenv('REDIR_TEST_DIR') . 'pdo_test.inc';
$db = PDOTest::factory();
PDOTest::dropTableIfExists($db, "pdo_stmt_cyclic_ref");
?>
--EXPECTF--
Column fetch:
string(1) "A"
string(1) "B"
string(1) "C"
Class fetch:
object(TestRow)#%d (4) {
["id"]=>
string(1) "1"
["val"]=>
string(1) "A"
["val2"]=>
string(2) "AA"
["arg"]=>
string(11) "Hello world"
}
object(TestRow)#%d (4) {
["id"]=>
string(1) "2"
["val"]=>
string(1) "B"
["val2"]=>
string(2) "BB"
["arg"]=>
string(11) "Hello world"
}
object(TestRow)#%d (4) {
["id"]=>
string(1) "3"
["val"]=>
string(1) "C"
["val2"]=>
string(2) "CC"
["arg"]=>
string(11) "Hello world"
}
Fetch into:
object(TestRow)#4 (4) {
["id"]=>
string(1) "1"
["val"]=>
string(1) "A"
["val2"]=>
string(2) "AA"
["arg"]=>
string(21) "I am being fetch into"
}
object(TestRow)#4 (4) {
["id"]=>
string(1) "2"
["val"]=>
string(1) "B"
["val2"]=>
string(2) "BB"
["arg"]=>
string(21) "I am being fetch into"
}
object(TestRow)#4 (4) {
["id"]=>
string(1) "3"
["val"]=>
string(1) "C"
["val2"]=>
string(2) "CC"
["arg"]=>
string(21) "I am being fetch into"
}
11 changes: 9 additions & 2 deletions ext/pdo_firebird/firebird_statement.c
Original file line number Diff line number Diff line change
Expand Up @@ -158,8 +158,15 @@ static int pdo_firebird_stmt_dtor(pdo_stmt_t *stmt) /* {{{ */
pdo_firebird_stmt *S = (pdo_firebird_stmt*)stmt->driver_data;
int result = 1;

/* release the statement */
if (isc_dsql_free_statement(S->H->isc_status, &S->stmt, DSQL_drop)) {
/* TODO: for master, move this check to a separate function shared between pdo drivers.
* pdo_pgsql and pdo_mysql do this exact same thing */
bool server_obj_usable = !Z_ISUNDEF(stmt->database_object_handle)
&& IS_OBJ_VALID(EG(objects_store).object_buckets[Z_OBJ_HANDLE(stmt->database_object_handle)])
&& !(OBJ_FLAGS(Z_OBJ(stmt->database_object_handle)) & IS_OBJ_FREE_CALLED);

/* release the statement.
* Note: if the server object is already gone then the statement was closed already as well. */
if (server_obj_usable && isc_dsql_free_statement(S->H->isc_status, &S->stmt, DSQL_drop)) {
php_firebird_error_stmt(stmt);
result = 0;
}
Expand Down
4 changes: 2 additions & 2 deletions ext/pdo_firebird/tests/transaction_access_mode.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ $dbh = getDbConnection();
@$dbh->exec('DROP TABLE transaction_access_mode');
unset($dbh);
?>
--EXPECT--
--EXPECTF--
========== Set attr in construct ==========
OK: writable
OK: readonly
Expand All @@ -154,7 +154,7 @@ array(1) {
readonly
bool(true)
OK: readonly
SQLSTATE[42000]: Syntax error or access violation: -817 attempted update during read-only transaction
SQLSTATE[%r(42000|25006)%r]: %r(Read only sql transaction|Syntax error or access violation)%r: -817 attempted update during read-only transaction
array(1) {
[0]=>
array(2) {
Expand Down
Loading