Commit 0d7a0a4703b for php.net

commit 0d7a0a4703ba6fd35d5ae9317a8c24b1bcc6a70d
Author: Weilin Du <108666168+LamentXU123@users.noreply.github.com>
Date:   Sat May 9 23:55:26 2026 +0800

    ext/intl: Throw TypeError for non-stringable timezone objects (#21990)

diff --git a/NEWS b/NEWS
index 4ace7d3dc46..4f8fe29d53e 100644
--- a/NEWS
+++ b/NEWS
@@ -62,6 +62,8 @@ PHP                                                                        NEWS
   . Fixed bug GH-20426 (Spoofchecker::setRestrictionLevel() error message
     suggests missing constants). (DanielEScherzer)
   . Added grapheme_strrev (Yuya Hamada)
+  . Passing a non-stringable object as a time zone to Intl time zone
+    argument handling now raises TypeError instead of Error. (Weilin Du)

 - JSON:
   . Enriched JSON last error / exception message with error location.
diff --git a/UPGRADING b/UPGRADING
index d271eb47f7d..f4853af485f 100644
--- a/UPGRADING
+++ b/UPGRADING
@@ -34,6 +34,10 @@ PHP 8.6 UPGRADE NOTES
     array arguments types/values and raise a TypeError/ValueError
     accordingly.

+- Intl:
+  . Passing a non-stringable object as a time zone to Intl APIs that accept
+    time zone objects or strings now raises a TypeError instead of an Error.
+
 - PCNTL:
   . pcntl_alarm() now raises a ValueError if the seconds argument is
     lower than zero or greater than platform's UINT_MAX.
diff --git a/ext/intl/calendar/calendar_methods.cpp b/ext/intl/calendar/calendar_methods.cpp
index 4828c641730..08e44680693 100644
--- a/ext/intl/calendar/calendar_methods.cpp
+++ b/ext/intl/calendar/calendar_methods.cpp
@@ -85,7 +85,7 @@ U_CFUNC PHP_FUNCTION(intlcal_create_instance)
 		Z_PARAM_STRING_OR_NULL(locale_str, locale_len)
 	ZEND_PARSE_PARAMETERS_END();

-	TimeZone *timeZone = timezone_process_timezone_argument(timezone_object, timezone_string, nullptr);
+	TimeZone *timeZone = timezone_process_timezone_argument(timezone_object, timezone_string, nullptr, 1);
 	if (timeZone == nullptr) {
 		RETURN_NULL();
 	}
@@ -316,7 +316,7 @@ U_CFUNC PHP_FUNCTION(intlcal_set_time_zone)
 	}

 	TimeZone *timeZone = timezone_process_timezone_argument(
-		timezone_object, timezone_string, CALENDAR_ERROR_P(co));
+		timezone_object, timezone_string, CALENDAR_ERROR_P(co), 2);
 	if (timeZone == nullptr) {
 		RETURN_FALSE;
 	}
@@ -345,7 +345,7 @@ U_CFUNC PHP_METHOD(IntlCalendar, setTimeZone)
 	}

 	TimeZone *timeZone = timezone_process_timezone_argument(
-		timezone_object, timezone_string, CALENDAR_ERROR_P(co));
+		timezone_object, timezone_string, CALENDAR_ERROR_P(co), 1);
 	if (timeZone == nullptr) {
 		RETURN_FALSE;
 	}
diff --git a/ext/intl/calendar/gregoriancalendar_methods.cpp b/ext/intl/calendar/gregoriancalendar_methods.cpp
index b94548b54a6..f79c6bf14c8 100644
--- a/ext/intl/calendar/gregoriancalendar_methods.cpp
+++ b/ext/intl/calendar/gregoriancalendar_methods.cpp
@@ -143,7 +143,7 @@ static void _php_intlgregcal_constructor_body(INTERNAL_FUNCTION_PARAMETERS, bool

 	if (variant <= 2) {
 		// From timezone and locale (0 to 2 arguments)
-		TimeZone *tz = timezone_process_timezone_argument(timezone_object, timezone_string, nullptr);
+		TimeZone *tz = timezone_process_timezone_argument(timezone_object, timezone_string, nullptr, 1);
 		if (tz == nullptr) {
 			// TODO: Exception should always occur already?
 			if (!EG(exception)) {
diff --git a/ext/intl/dateformat/dateformat_attrcpp.cpp b/ext/intl/dateformat/dateformat_attrcpp.cpp
index 1391823ff0f..1c8fb88a3d5 100644
--- a/ext/intl/dateformat/dateformat_attrcpp.cpp
+++ b/ext/intl/dateformat/dateformat_attrcpp.cpp
@@ -94,7 +94,7 @@ U_CFUNC PHP_FUNCTION(datefmt_set_timezone)
 	DATE_FORMAT_METHOD_FETCH_OBJECT;

 	TimeZone *timezone = timezone_process_timezone_argument(
-		timezone_object, timezone_string, INTL_DATA_ERROR_P(dfo));
+		timezone_object, timezone_string, INTL_DATA_ERROR_P(dfo), 2);
 	if (timezone == nullptr) {
 		RETURN_FALSE;
 	}
@@ -119,7 +119,7 @@ U_CFUNC PHP_METHOD(IntlDateFormatter, setTimeZone)
 	DATE_FORMAT_METHOD_FETCH_OBJECT;

 	TimeZone *timezone = timezone_process_timezone_argument(
-		timezone_object, timezone_string, INTL_DATA_ERROR_P(dfo));
+		timezone_object, timezone_string, INTL_DATA_ERROR_P(dfo), 1);
 	if (timezone == nullptr) {
 		RETURN_FALSE;
 	}
diff --git a/ext/intl/dateformat/dateformat_create.cpp b/ext/intl/dateformat/dateformat_create.cpp
index 4b055fc88eb..574e00099d4 100644
--- a/ext/intl/dateformat/dateformat_create.cpp
+++ b/ext/intl/dateformat/dateformat_create.cpp
@@ -131,7 +131,7 @@ static zend_result datefmt_ctor(INTERNAL_FUNCTION_PARAMETERS)

 	if (explicit_tz || calendar_owned ) {
 		//we have an explicit time zone or a non-object calendar
-		timezone = timezone_process_timezone_argument(timezone_object, timezone_string, INTL_DATA_ERROR_P(dfo));
+		timezone = timezone_process_timezone_argument(timezone_object, timezone_string, INTL_DATA_ERROR_P(dfo), 4);
 		if (timezone == nullptr) {
 			goto error;
 		}
diff --git a/ext/intl/msgformat/msgformat_helpers.cpp b/ext/intl/msgformat/msgformat_helpers.cpp
index 399db1e8c0b..f504ee50abc 100644
--- a/ext/intl/msgformat/msgformat_helpers.cpp
+++ b/ext/intl/msgformat/msgformat_helpers.cpp
@@ -341,7 +341,7 @@ static void umsg_set_timezone(MessageFormatter_object *mfo,
 		}

 		if (used_tz == NULL) {
-			used_tz = timezone_process_timezone_argument(nullptr, nullptr, &err);
+			used_tz = timezone_process_timezone_argument(nullptr, nullptr, &err, 1);
 			if (used_tz == NULL) {
 				continue;
 			}
diff --git a/ext/intl/tests/timezone_argument_type_errors.phpt b/ext/intl/tests/timezone_argument_type_errors.phpt
new file mode 100644
index 00000000000..02e1c58492c
--- /dev/null
+++ b/ext/intl/tests/timezone_argument_type_errors.phpt
@@ -0,0 +1,42 @@
+--TEST--
+Intl timezone argument APIs reject non-stringable objects with TypeError
+--EXTENSIONS--
+intl
+--FILE--
+<?php
+
+function dump_exception(callable $cb): void {
+	try {
+		$cb();
+	} catch (Throwable $e) {
+		echo $e::class, ': ', $e->getMessage(), PHP_EOL;
+	}
+}
+
+$std = new stdClass();
+$calendar = IntlCalendar::createInstance();
+$formatter = new IntlDateFormatter(null, IntlDateFormatter::NONE, IntlDateFormatter::NONE);
+
+dump_exception(fn() => intlcal_create_instance($std));
+dump_exception(fn() => IntlCalendar::createInstance($std));
+dump_exception(fn() => intlcal_set_time_zone($calendar, $std));
+dump_exception(fn() => $calendar->setTimeZone($std));
+dump_exception(fn() => new IntlGregorianCalendar($std));
+dump_exception(fn() => datefmt_create(null, IntlDateFormatter::NONE, IntlDateFormatter::NONE, $std));
+dump_exception(fn() => IntlDateFormatter::create(null, IntlDateFormatter::NONE, IntlDateFormatter::NONE, $std));
+dump_exception(fn() => new IntlDateFormatter(null, IntlDateFormatter::NONE, IntlDateFormatter::NONE, $std));
+dump_exception(fn() => datefmt_set_timezone($formatter, $std));
+dump_exception(fn() => $formatter->setTimeZone($std));
+
+?>
+--EXPECT--
+TypeError: intlcal_create_instance(): Argument #1 ($timezone) Object of class stdClass could not be converted to string
+TypeError: IntlCalendar::createInstance(): Argument #1 ($timezone) Object of class stdClass could not be converted to string
+TypeError: intlcal_set_time_zone(): Argument #2 ($timezone) Object of class stdClass could not be converted to string
+TypeError: IntlCalendar::setTimeZone(): Argument #1 ($timezone) Object of class stdClass could not be converted to string
+TypeError: IntlGregorianCalendar::__construct(): Argument #1 ($timezoneOrYear) Object of class stdClass could not be converted to string
+TypeError: datefmt_create(): Argument #4 ($timezone) Object of class stdClass could not be converted to string
+TypeError: IntlDateFormatter::create(): Argument #4 ($timezone) Object of class stdClass could not be converted to string
+TypeError: IntlDateFormatter::__construct(): Argument #4 ($timezone) Object of class stdClass could not be converted to string
+TypeError: datefmt_set_timezone(): Argument #2 ($timezone) Object of class stdClass could not be converted to string
+TypeError: IntlDateFormatter::setTimeZone(): Argument #1 ($timezone) Object of class stdClass could not be converted to string
diff --git a/ext/intl/timezone/timezone_class.cpp b/ext/intl/timezone/timezone_class.cpp
index 8aad4fb2b6b..0d29a47c186 100644
--- a/ext/intl/timezone/timezone_class.cpp
+++ b/ext/intl/timezone/timezone_class.cpp
@@ -121,7 +121,7 @@ static void timezone_throw_exception_with_call_location(const char *msg, const c
 /* {{{ timezone_process_timezone_argument
  * TimeZone argument processor. outside_error may be nullptr (for static functions/constructors) */
 U_CFUNC TimeZone *timezone_process_timezone_argument(
-	zend_object *timezone_object, zend_string *timezone_string, intl_error *outside_error)
+	zend_object *timezone_object, zend_string *timezone_string, intl_error *outside_error, uint32_t arg_num)
 {
 	std::unique_ptr<TimeZone>	timeZone;
 	bool free_string = false;
@@ -160,8 +160,9 @@ U_CFUNC TimeZone *timezone_process_timezone_argument(
 				free_string = true;
 			} else {
 				if (!EG(exception)) {
-					// TODO Proper type error
-					zend_throw_error(nullptr, "Object of class %s could not be converted to string", ZSTR_VAL(timezone_object->ce->name));
+					zend_argument_type_error(arg_num,
+						"Object of class %s could not be converted to string",
+						ZSTR_VAL(timezone_object->ce->name));
 				}
 				return nullptr;
 			}
diff --git a/ext/intl/timezone/timezone_class.h b/ext/intl/timezone/timezone_class.h
index b383db558fc..70b3b5637f2 100644
--- a/ext/intl/timezone/timezone_class.h
+++ b/ext/intl/timezone/timezone_class.h
@@ -67,7 +67,7 @@ static inline TimeZone_object *php_intl_timezone_fetch_object(zend_object *obj)
 	}

 zval *timezone_convert_to_datetimezone(const TimeZone *timeZone, intl_error *outside_error, zval *ret);
-TimeZone *timezone_process_timezone_argument(zend_object *timezone_object, zend_string *timezone_string, intl_error *error);
+TimeZone *timezone_process_timezone_argument(zend_object *timezone_object, zend_string *timezone_string, intl_error *error, uint32_t arg_num);

 void timezone_object_construct(const TimeZone *zone, zval *object, int owned);