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