Commit 392a84d7e1d for php.net
commit 392a84d7e1d39a55081bf00ee49f85cf45412d06
Author: David Carlier <devnexen@gmail.com>
Date: Mon Apr 27 12:39:01 2026 +0100
ext/pgsql: clean up pg_fetch_object() constructor handling.
Move object_init_ex() before php_pgsql_fetch_hash() so abstract,
interface and enum errors surface earlier, look up the constructor
via the get_constructor handler (under EG(fake_scope)) so classes
with overridden handlers work, and mark the object as ctor-failed
when the constructor throws.
The ValueError for ctor args on a class without a constructor now
reports argument 4 ($constructor_args) instead of argument 3 ($class).
close GH-21884
diff --git a/NEWS b/NEWS
index 3da5464750c..3144cdf88c5 100644
--- a/NEWS
+++ b/NEWS
@@ -104,6 +104,10 @@ PHP NEWS
- PGSQL:
. Enabled 64 bits support for pg_lo_truncate()/pg_lo_tell()
if the server supports it. (KentarouTakeda)
+ . pg_fetch_object() now surfaces non-instantiable class errors
+ before fetching, resolves the constructor via the get_constructor
+ handler, and reports the empty-constructor ValueError on the
+ $constructor_args argument. (David Carlier)
- Phar:
. Support reference values in Phar::mungServer(). (ndossche)
diff --git a/UPGRADING b/UPGRADING
index 56049f30cf9..6777820642f 100644
--- a/UPGRADING
+++ b/UPGRADING
@@ -47,6 +47,13 @@ PHP 8.6 UPGRADE NOTES
. Phar::mungServer() now raises a ValueError when an invalid
argument value is passed instead of being silently ignored.
+- PGSQL:
+ . pg_fetch_object() now reports the ValueError for a non-empty
+ $constructor_args on a class without a constructor on the
+ $constructor_args argument instead of $class. Errors raised when
+ the requested class is not instantiable (abstract, interface, enum)
+ now surface before the row is fetched.
+
- Posix:
. posix_access() now raises a ValueError when an invalid $flags
argument value is passed.
diff --git a/ext/pgsql/pgsql.c b/ext/pgsql/pgsql.c
index 1674625429a..8cb022c79cd 100644
--- a/ext/pgsql/pgsql.c
+++ b/ext/pgsql/pgsql.c
@@ -2129,26 +2129,17 @@ PHP_FUNCTION(pg_fetch_object)
ce = zend_standard_class_def;
}
- if (!ce->constructor && ctor_params && zend_hash_num_elements(ctor_params) > 0) {
- zend_argument_value_error(3,
- "must be empty when the specified class (%s) does not have a constructor",
- ZSTR_VAL(ce->name)
- );
+ if (UNEXPECTED(object_init_ex(return_value, ce) == FAILURE)) {
RETURN_THROWS();
}
zval dataset;
if (UNEXPECTED(!php_pgsql_fetch_hash(&dataset, result, row, row_is_null, PGSQL_ASSOC))) {
/* Either an exception is thrown, or we return false */
+ zval_ptr_dtor(return_value);
RETURN_FALSE;
}
- // TODO: Check CE is an instantiable class earlier?
- zend_result obj_initialized = object_init_ex(return_value, ce);
- if (UNEXPECTED(obj_initialized == FAILURE)) {
- zval_ptr_dtor(&dataset);
- RETURN_THROWS();
- }
if (!ce->default_properties_count && !ce->__set) {
Z_OBJ_P(return_value)->properties = Z_ARR(dataset);
} else {
@@ -2156,10 +2147,32 @@ PHP_FUNCTION(pg_fetch_object)
zval_ptr_dtor(&dataset);
}
- // TODO: Need to grab constructor via object handler as this allows instantiating internal objects with overridden get_constructor
- if (ce->constructor) {
- zend_call_known_function(ce->constructor, Z_OBJ_P(return_value), Z_OBJCE_P(return_value),
+ zend_object *obj = Z_OBJ_P(return_value);
+ const zend_class_entry *old = EG(fake_scope);
+ EG(fake_scope) = ce;
+ zend_function *constructor = obj->handlers->get_constructor(obj);
+ EG(fake_scope) = old;
+
+ if (UNEXPECTED(EG(exception))) {
+ /* visibility error or override refused - VM dtors return_value */
+ return;
+ }
+
+ if (UNEXPECTED(!constructor && ctor_params && zend_hash_num_elements(ctor_params) > 0)) {
+ zend_argument_value_error(4,
+ "must be empty when the specified class (%s) does not have a constructor",
+ ZSTR_VAL(ce->name)
+ );
+ RETURN_THROWS();
+ }
+
+ if (constructor) {
+ zend_call_known_function(constructor, obj, ce,
/* retval */ NULL, /* argc */ 0, /* params */ NULL, ctor_params);
+ if (EG(exception)) {
+ zend_object_store_ctor_failed(obj);
+ RETURN_THROWS();
+ }
}
}
/* }}} */
diff --git a/ext/pgsql/tests/pg_fetch_object_ctor_paths.phpt b/ext/pgsql/tests/pg_fetch_object_ctor_paths.phpt
new file mode 100644
index 00000000000..bd3df73acba
--- /dev/null
+++ b/ext/pgsql/tests/pg_fetch_object_ctor_paths.phpt
@@ -0,0 +1,69 @@
+--TEST--
+pg_fetch_object() constructor handling: ctor_params validation, throwing constructor, property visibility
+--EXTENSIONS--
+pgsql
+--SKIPIF--
+<?php include("inc/skipif.inc"); ?>
+--FILE--
+<?php
+include 'inc/config.inc';
+
+class NoCtor {}
+
+class ThrowingCtor {
+ public function __construct() {
+ throw new RuntimeException('boom');
+ }
+ public function __destruct() {
+ echo "ThrowingCtor::__destruct called (BUG)\n";
+ }
+}
+
+class SeesProps {
+ public function __construct() {
+ echo "ctor sees: num={$this->num}, str={$this->str}\n";
+ }
+ public function __destruct() {
+ echo "SeesProps::__destruct called\n";
+ }
+}
+
+$table_name = "pg_fetch_object_ctor_paths";
+$db = pg_connect($conn_str);
+pg_query($db, "CREATE TABLE {$table_name} (num int, str text)");
+pg_query($db, "INSERT INTO {$table_name} VALUES(1, 'hello')");
+
+$sql = "SELECT * FROM {$table_name} WHERE num = 1";
+
+// 1) ctor_params on a class with no constructor must throw ValueError
+try {
+ pg_fetch_object(pg_query($db, $sql), null, 'NoCtor', [1, 2]);
+} catch (ValueError $e) {
+ echo $e->getMessage(), "\n";
+}
+
+// 2) Constructor that throws: __destruct must NOT run on the partially constructed object
+try {
+ pg_fetch_object(pg_query($db, $sql), null, 'ThrowingCtor');
+} catch (RuntimeException $e) {
+ echo "caught: ", $e->getMessage(), "\n";
+}
+
+// 3) Constructor sees row properties already merged onto $this
+$obj = pg_fetch_object(pg_query($db, $sql), null, 'SeesProps');
+unset($obj);
+
+echo "Ok\n";
+?>
+--CLEAN--
+<?php
+include('inc/config.inc');
+$db = pg_connect($conn_str);
+pg_query($db, "DROP TABLE IF EXISTS pg_fetch_object_ctor_paths");
+?>
+--EXPECT--
+pg_fetch_object(): Argument #4 ($constructor_args) must be empty when the specified class (NoCtor) does not have a constructor
+caught: boom
+ctor sees: num=1, str=hello
+SeesProps::__destruct called
+Ok