From 7cb62fd21d553f94a0ec370379c44c9a5ca6e21f Mon Sep 17 00:00:00 2001 From: David Carlier Date: Sun, 11 Oct 2026 19:46:21 +0100 Subject: [PATCH 1/5] ext/intl: Add IntlNumberFormatter skeleton based formatter. Add IntlNumberFormatter::createFromSkeleton() so format() can render units such as "5 km/h", which NumberFormatter cannot do. Needs ICU 63+. Close GH-24165 --- ext/intl/config.m4 | 2 + ext/intl/config.w32 | 3 + .../numberformatter/numberformatter.stub.php | 19 ++ .../numberformatter/numberformatter_arginfo.h | 45 +++++ .../numberformatter/numberformatter_class.cpp | 168 ++++++++++++++++++ .../numberformatter/numberformatter_class.h | 63 +++++++ ext/intl/php_intl.c | 4 + ext/intl/tests/numberformatter/basic.phpt | 58 ++++++ ext/intl/tests/numberformatter/errors.phpt | 76 ++++++++ .../tests/numberformatter/format_failure.phpt | 48 +++++ .../tests/numberformatter/format_int64.phpt | 25 +++ 11 files changed, 511 insertions(+) create mode 100644 ext/intl/numberformatter/numberformatter.stub.php create mode 100644 ext/intl/numberformatter/numberformatter_arginfo.h create mode 100644 ext/intl/numberformatter/numberformatter_class.cpp create mode 100644 ext/intl/numberformatter/numberformatter_class.h create mode 100644 ext/intl/tests/numberformatter/basic.phpt create mode 100644 ext/intl/tests/numberformatter/errors.phpt create mode 100644 ext/intl/tests/numberformatter/format_failure.phpt create mode 100644 ext/intl/tests/numberformatter/format_int64.phpt diff --git a/ext/intl/config.m4 b/ext/intl/config.m4 index f6a01053df20..0f426e45af73 100644 --- a/ext/intl/config.m4 +++ b/ext/intl/config.m4 @@ -48,6 +48,7 @@ if test "$PHP_INTL" != "no"; then grapheme/grapheme_string.cpp \ grapheme/grapheme_util.cpp \ msgformat/msgformat_helpers.cpp \ + numberformatter/numberformatter_class.cpp \ rangeformatter/rangeformatter_class.cpp \ timezone/timezone_class.cpp \ timezone/timezone_methods.cpp \ @@ -126,6 +127,7 @@ if test "$PHP_INTL" != "no"; then $ext_builddir/listformatter $ext_builddir/msgformat $ext_builddir/normalizer + $ext_builddir/numberformatter $ext_builddir/rangeformatter $ext_builddir/resourcebundle $ext_builddir/spoofchecker diff --git a/ext/intl/config.w32 b/ext/intl/config.w32 index 3cde431ce66e..8ca7096152b3 100644 --- a/ext/intl/config.w32 +++ b/ext/intl/config.w32 @@ -63,6 +63,9 @@ if (PHP_INTL != "no") { normalizer_class.cpp \ normalizer_normalize.cpp \ ", "intl"); + ADD_SOURCES(configure_module_dirname + "/numberformatter", "\ + numberformatter_class.cpp \ + ", "intl"); ADD_SOURCES(configure_module_dirname + "/rangeformatter", "\ rangeformatter_class.cpp \ ", "intl"); diff --git a/ext/intl/numberformatter/numberformatter.stub.php b/ext/intl/numberformatter/numberformatter.stub.php new file mode 100644 index 000000000000..167710defea7 --- /dev/null +++ b/ext/intl/numberformatter/numberformatter.stub.php @@ -0,0 +1,19 @@ +. | + | | + | SPDX-License-Identifier: BSD-3-Clause | + +----------------------------------------------------------------------+ + | Authors: David Carlier | + +----------------------------------------------------------------------+ +*/ + +extern "C" { +#include "php.h" +#include "zend_API.h" +#include "../intl_common.h" +} + +#if U_ICU_VERSION_MAJOR_NUM >= 63 +#include +#include +#include +#include "../intl_convertcpp.h" + +extern "C" { +#include "../php_intl.h" +#include "numberformatter_arginfo.h" +} +#include "numberformatter_class.h" + +using icu::number::NumberFormatter; +using icu::number::UnlocalizedNumberFormatter; +using icu::UnicodeString; + +static zend_object_handlers numberformatter_handlers; +zend_class_entry *IntlNumberFormatter_ce_ptr; + +static void numberformatter_free_obj(zend_object *object) +{ + IntlNumberFormatter_object *nfo = php_intl_numberformatter_fetch_object(object); + + delete NUMBERFORMATTER_OBJECT(nfo); + NUMBERFORMATTER_OBJECT(nfo) = nullptr; + intl_error_reset(INTL_DATA_ERROR_P(nfo)); + + zend_object_std_dtor(&nfo->zo); +} + +static zend_object *numberformatter_create_object(zend_class_entry *ce) +{ + IntlNumberFormatter_object *nfo = reinterpret_cast(zend_object_alloc(sizeof(IntlNumberFormatter_object), ce)); + + intl_error_init(INTL_DATA_ERROR_P(nfo)); + NUMBERFORMATTER_OBJECT(nfo) = nullptr; + + zend_object_std_init(&nfo->zo, ce); + object_properties_init(&nfo->zo, ce); + nfo->zo.handlers = &numberformatter_handlers; + + return &nfo->zo; +} + +U_CFUNC PHP_METHOD(IntlNumberFormatter, __construct) +{ + ZEND_PARSE_PARAMETERS_NONE(); + + zend_throw_error(nullptr, "Cannot directly construct %s, use createFromSkeleton method instead", ZSTR_VAL(Z_OBJCE_P(ZEND_THIS)->name)); +} + +U_CFUNC PHP_METHOD(IntlNumberFormatter, createFromSkeleton) +{ + char *skeleton, *locale; + size_t skeleton_len, locale_len; + + ZEND_PARSE_PARAMETERS_START(2, 2) + Z_PARAM_STRING(skeleton, skeleton_len) + Z_PARAM_STRING(locale, locale_len) + ZEND_PARSE_PARAMETERS_END(); + + intl_error_reset(nullptr); + + INTL_CHECK_LOCALE_LEN(locale_len); + + if (locale_len == 0) { + locale = (char *)intl_locale_get_default(); + } + + if (icu::Locale(locale).getISO3Language()[0] == '\0') { + zend_argument_value_error(2, "\"%s\" is invalid", locale); + RETURN_THROWS(); + } + + UErrorCode status = U_ZERO_ERROR; + UnicodeString uskeleton; + + intl_stringFromChar(uskeleton, skeleton, skeleton_len, &status); + INTL_CHECK_STATUS_OR_NULL(status, "Error converting skeleton to UTF-16"); + + UnlocalizedNumberFormatter nf = NumberFormatter::forSkeleton(uskeleton, status); + INTL_CHECK_STATUS_OR_NULL(status, "Number formatter creation failed"); + + object_init_ex(return_value, IntlNumberFormatter_ce_ptr); + NUMBERFORMATTER_OBJECT(Z_INTL_INTLNUMBERFORMATTER_P(return_value)) = new LocalizedNumberFormatter(nf.locale(icu::Locale(locale))); +} + +U_CFUNC PHP_METHOD(IntlNumberFormatter, format) +{ + zval *value; + NUMBERFORMATTER_METHOD_INIT_VARS; + + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_NUMBER(value) + ZEND_PARSE_PARAMETERS_END(); + + object = ZEND_THIS; + NUMBERFORMATTER_METHOD_FETCH_OBJECT; + + UErrorCode status = U_ZERO_ERROR; + UnicodeString result; + + if (Z_TYPE_P(value) == IS_DOUBLE) { + result = NUMBERFORMATTER_OBJECT(nfo)->formatDouble(Z_DVAL_P(value), status).toString(status); + } else { + result = NUMBERFORMATTER_OBJECT(nfo)->formatInt(static_cast(Z_LVAL_P(value)), status).toString(status); + } + + if (U_FAILURE(status)) { + intl_errors_set(INTL_DATA_ERROR_P(nfo), status, "Number formatting failed"); + RETURN_FALSE; + } + + zend_string *ret = intl_charFromString(result, &status); + + if (!ret) { + intl_errors_set(INTL_DATA_ERROR_P(nfo), status, "Error converting value to UTF-8"); + RETURN_FALSE; + } + + RETVAL_STR(ret); +} + +U_CFUNC PHP_METHOD(IntlNumberFormatter, getErrorCode) +{ + ZEND_PARSE_PARAMETERS_NONE(); + + RETURN_LONG(INTL_DATA_ERROR_CODE(Z_INTL_INTLNUMBERFORMATTER_P(ZEND_THIS))); +} + +U_CFUNC PHP_METHOD(IntlNumberFormatter, getErrorMessage) +{ + ZEND_PARSE_PARAMETERS_NONE(); + + RETURN_STR(intl_error_get_message(INTL_DATA_ERROR_P(Z_INTL_INTLNUMBERFORMATTER_P(ZEND_THIS)))); +} + +void numberformatter_register_class(void) +{ + IntlNumberFormatter_ce_ptr = register_class_IntlNumberFormatter(); + IntlNumberFormatter_ce_ptr->create_object = numberformatter_create_object; + + memcpy(&numberformatter_handlers, zend_get_std_object_handlers(), sizeof(zend_object_handlers)); + numberformatter_handlers.offset = offsetof(IntlNumberFormatter_object, zo); + numberformatter_handlers.free_obj = numberformatter_free_obj; + numberformatter_handlers.clone_obj = nullptr; +} +#endif diff --git a/ext/intl/numberformatter/numberformatter_class.h b/ext/intl/numberformatter/numberformatter_class.h new file mode 100644 index 000000000000..2215791b9146 --- /dev/null +++ b/ext/intl/numberformatter/numberformatter_class.h @@ -0,0 +1,63 @@ +/* + +----------------------------------------------------------------------+ + | Copyright © The PHP Group and Contributors. | + +----------------------------------------------------------------------+ + | This source file is subject to the Modified BSD License that is | + | bundled with this package in the file LICENSE, and is available | + | through the World Wide Web at . | + | | + | SPDX-License-Identifier: BSD-3-Clause | + +----------------------------------------------------------------------+ + | Authors: David Carlier | + +----------------------------------------------------------------------+ +*/ + +#ifndef NUMBERFORMATTER_CLASS_H +#define NUMBERFORMATTER_CLASS_H + +#ifdef __cplusplus +extern "C" { +#endif +#include +#include "intl_common.h" +#include "intl_error.h" +#include "intl_data.h" +#ifdef __cplusplus +} +#endif + +#ifdef __cplusplus +#include +using icu::number::LocalizedNumberFormatter; +#else +typedef void LocalizedNumberFormatter; +#endif + +typedef struct { + intl_error error; + LocalizedNumberFormatter *unumf; +} numberformatter_data; + +typedef struct { + numberformatter_data nf_data; + zend_object zo; +} IntlNumberFormatter_object; + +#define php_intl_numberformatter_fetch_object(obj) ZEND_CONTAINER_OF(obj, IntlNumberFormatter_object, zo) +#define Z_INTL_INTLNUMBERFORMATTER_P(zv) php_intl_numberformatter_fetch_object(Z_OBJ_P(zv)) + +#define NUMBERFORMATTER_OBJECT(nfo) (nfo)->nf_data.unumf + +#define NUMBERFORMATTER_METHOD_INIT_VARS INTL_METHOD_INIT_VARS(IntlNumberFormatter, nfo) +#define NUMBERFORMATTER_METHOD_FETCH_OBJECT INTL_METHOD_FETCH_OBJECT(INTL_INTLNUMBERFORMATTER, nfo) + +#ifdef __cplusplus +extern "C" { +#endif +void numberformatter_register_class(void); +extern zend_class_entry *IntlNumberFormatter_ce_ptr; +#ifdef __cplusplus +} +#endif + +#endif diff --git a/ext/intl/php_intl.c b/ext/intl/php_intl.c index ce938b915595..5b51d98dba04 100644 --- a/ext/intl/php_intl.c +++ b/ext/intl/php_intl.c @@ -42,6 +42,7 @@ #include "locale/locale_class.h" #include "listformatter/listformatter_class.h" +#include "numberformatter/numberformatter_class.h" #include "rangeformatter/rangeformatter_class.h" #include "dateformat/dateformat.h" @@ -190,6 +191,9 @@ PHP_MINIT_FUNCTION( intl ) listformatter_register_class( ); #if U_ICU_VERSION_MAJOR_NUM >= 63 + /* Register 'IntlNumberFormatter' PHP class */ + numberformatter_register_class( ); + /* Register 'NumberRangeFormatter' PHP class */ rangeformatter_register_class( ); #endif diff --git a/ext/intl/tests/numberformatter/basic.phpt b/ext/intl/tests/numberformatter/basic.phpt new file mode 100644 index 000000000000..e499571962fa --- /dev/null +++ b/ext/intl/tests/numberformatter/basic.phpt @@ -0,0 +1,58 @@ +--TEST-- +Basic test for IntlNumberFormatter +--EXTENSIONS-- +intl +--SKIPIF-- + +--FILE-- +format($value)); +} + +var_dump($formatter->getErrorCode()); +var_dump($formatter->getErrorMessage()); + +?> +--EXPECT-- +string(6) "5 km/h" +string(21) "5 kilometers per hour" +string(20) "1 kilometer per hour" +string(23) "1.5 kilometers per hour" +string(24) "1,5 Kilometer pro Stunde" +string(5) "5 m/s" +string(5) "1.5GB" +string(14) "1,234.50 euros" +string(3) "25%" +string(4) "1.2M" +string(8) "1,234.50" +string(7) "1234567" +string(2) "+5" +string(10) "1,234.5678" +string(3) "NaN" +int(0) +string(12) "U_ZERO_ERROR" diff --git a/ext/intl/tests/numberformatter/errors.phpt b/ext/intl/tests/numberformatter/errors.phpt new file mode 100644 index 000000000000..f04e24b025fc --- /dev/null +++ b/ext/intl/tests/numberformatter/errors.phpt @@ -0,0 +1,76 @@ +--TEST-- +Errors for IntlNumberFormatter +--EXTENSIONS-- +intl +--SKIPIF-- + +--FILE-- +getMessage(), PHP_EOL; +} + +ini_set('intl.use_exceptions', '0'); + +try { + new IntlNumberFormatter(); +} catch (Error $e) { + echo $e::class, ': ', $e->getMessage(), PHP_EOL; +} + +try { + IntlNumberFormatter::createFromSkeleton('', 'invalid-language'); +} catch (ValueError $e) { + echo $e::class, ': ', $e->getMessage(), PHP_EOL; +} + +try { + $formatter->format([]); +} catch (TypeError $e) { + echo $e::class, ': ', $e->getMessage(), PHP_EOL; +} + +try { + clone $formatter; +} catch (Error $e) { + echo $e::class, ': ', $e->getMessage(), PHP_EOL; +} + +?> +--EXPECT-- +NULL +int(65811) +string(107) "IntlNumberFormatter::createFromSkeleton(): Number formatter creation failed: U_NUMBER_SKELETON_SYNTAX_ERROR" +NULL +string(107) "IntlNumberFormatter::createFromSkeleton(): Number formatter creation failed: U_NUMBER_SKELETON_SYNTAX_ERROR" +NULL +string(132) "IntlNumberFormatter::createFromSkeleton(): Locale string too long, should be no longer than 156 characters: U_ILLEGAL_ARGUMENT_ERROR" +int(0) +IntlException: IntlNumberFormatter::createFromSkeleton(): Number formatter creation failed +Error: Call to private IntlNumberFormatter::__construct() from global scope +ValueError: IntlNumberFormatter::createFromSkeleton(): Argument #2 ($locale) "invalid-language" is invalid +TypeError: IntlNumberFormatter::format(): Argument #1 ($value) must be of type int|float, array given +Error: Trying to clone an uncloneable object of class IntlNumberFormatter diff --git a/ext/intl/tests/numberformatter/format_failure.phpt b/ext/intl/tests/numberformatter/format_failure.phpt new file mode 100644 index 000000000000..ff91a9ca1bbf --- /dev/null +++ b/ext/intl/tests/numberformatter/format_failure.phpt @@ -0,0 +1,48 @@ +--TEST-- +IntlNumberFormatter::format() with a failing formatter +--EXTENSIONS-- +intl +--SKIPIF-- + +--FILE-- +format(1)); +var_dump($formatter->getErrorCode() === U_UNSUPPORTED_ERROR); +var_dump($formatter->getErrorMessage()); +var_dump(intl_get_error_code() === U_UNSUPPORTED_ERROR); + +ini_set('intl.use_exceptions', '1'); + +try { + $formatter->format(1); +} catch (IntlException $e) { + echo $e::class, ': ', $e->getMessage(), PHP_EOL; +} + +ini_set('intl.use_exceptions', '0'); + +$formatter = IntlNumberFormatter::createFromSkeleton('', 'en_US'); + +var_dump($formatter->format(1)); +var_dump(intl_get_error_code()); +var_dump($formatter->getErrorCode()); +var_dump($formatter->getErrorMessage()); + +?> +--EXPECT-- +bool(false) +bool(true) +string(76) "IntlNumberFormatter::format(): Number formatting failed: U_UNSUPPORTED_ERROR" +bool(true) +IntlException: IntlNumberFormatter::format(): Number formatting failed +string(1) "1" +int(0) +int(0) +string(12) "U_ZERO_ERROR" diff --git a/ext/intl/tests/numberformatter/format_int64.phpt b/ext/intl/tests/numberformatter/format_int64.phpt new file mode 100644 index 000000000000..28e706232436 --- /dev/null +++ b/ext/intl/tests/numberformatter/format_int64.phpt @@ -0,0 +1,25 @@ +--TEST-- +IntlNumberFormatter::format() preserves int64 precision +--EXTENSIONS-- +intl +--SKIPIF-- + +--FILE-- +format(9007199254740993)); +var_dump($formatter->format(PHP_INT_MIN)); + +?> +--EXPECT-- +string(21) "9,007,199,254,740,993" +string(26) "-9,223,372,036,854,775,808" From d1381a876734b09fe7743b96b760fc6855c4bcb0 Mon Sep 17 00:00:00 2001 From: David Carlier Date: Sun, 11 Oct 2026 20:21:42 +0100 Subject: [PATCH 2/5] IntlNumberFormatter::format() accepts numeric strings. --- UPGRADING | 6 ++++++ ext/intl/numberformatter/numberformatter.stub.php | 2 +- ext/intl/numberformatter/numberformatter_arginfo.h | 4 ++-- ext/intl/numberformatter/numberformatter_class.cpp | 14 ++++++++++++-- ext/intl/tests/numberformatter/errors.phpt | 7 ++++++- 5 files changed, 27 insertions(+), 6 deletions(-) diff --git a/UPGRADING b/UPGRADING index 46cf4cafab12..07d223c3f6c0 100644 --- a/UPGRADING +++ b/UPGRADING @@ -68,6 +68,12 @@ PHP 8.7 UPGRADE NOTES 7. New Classes and Interfaces ======================================== +- Intl: + . Added IntlNumberFormatter class to format a number with a given locale + and ICU number skeleton, which also covers measurement units, e.g. + "measure-unit/speed-kilometer-per-hour unit-width-full-name". + It is supported from icu 63. + ======================================== 8. Removed Extensions and SAPIs ======================================== diff --git a/ext/intl/numberformatter/numberformatter.stub.php b/ext/intl/numberformatter/numberformatter.stub.php index 167710defea7..c98bb3cee389 100644 --- a/ext/intl/numberformatter/numberformatter.stub.php +++ b/ext/intl/numberformatter/numberformatter.stub.php @@ -11,7 +11,7 @@ private function __construct() {} public static function createFromSkeleton(string $skeleton, string $locale): ?IntlNumberFormatter {} - public function format(float|int $value): string|false {} + public function format(float|int|string $value): string|false {} public function getErrorCode(): int {} diff --git a/ext/intl/numberformatter/numberformatter_arginfo.h b/ext/intl/numberformatter/numberformatter_arginfo.h index 5b3365ea234e..065367dd2b27 100644 --- a/ext/intl/numberformatter/numberformatter_arginfo.h +++ b/ext/intl/numberformatter/numberformatter_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit numberformatter.stub.php instead. - * Stub hash: f29b6ea3231370e69a03ef6c39902b175163647a */ + * Stub hash: f99ec6e872e46e2274887cc608eb336417c52c78 */ ZEND_BEGIN_ARG_INFO_EX(arginfo_class_IntlNumberFormatter___construct, 0, 0, 0) ZEND_END_ARG_INFO() @@ -10,7 +10,7 @@ ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_IntlNumberFormatter_createF ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_WITH_RETURN_TYPE_MASK_EX(arginfo_class_IntlNumberFormatter_format, 0, 1, MAY_BE_STRING|MAY_BE_FALSE) - ZEND_ARG_TYPE_MASK(0, value, MAY_BE_DOUBLE|MAY_BE_LONG, NULL) + ZEND_ARG_TYPE_MASK(0, value, MAY_BE_DOUBLE|MAY_BE_LONG|MAY_BE_STRING, NULL) ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_IntlNumberFormatter_getErrorCode, 0, 0, IS_LONG, 0) diff --git a/ext/intl/numberformatter/numberformatter_class.cpp b/ext/intl/numberformatter/numberformatter_class.cpp index 60fb55325fcb..e59a7f6f9b78 100644 --- a/ext/intl/numberformatter/numberformatter_class.cpp +++ b/ext/intl/numberformatter/numberformatter_class.cpp @@ -111,7 +111,7 @@ U_CFUNC PHP_METHOD(IntlNumberFormatter, format) NUMBERFORMATTER_METHOD_INIT_VARS; ZEND_PARSE_PARAMETERS_START(1, 1) - Z_PARAM_NUMBER(value) + Z_PARAM_NUMBER_OR_STR(value) ZEND_PARSE_PARAMETERS_END(); object = ZEND_THIS; @@ -120,7 +120,17 @@ U_CFUNC PHP_METHOD(IntlNumberFormatter, format) UErrorCode status = U_ZERO_ERROR; UnicodeString result; - if (Z_TYPE_P(value) == IS_DOUBLE) { + if (Z_TYPE_P(value) == IS_STRING) { + if (UNEXPECTED(zend_str_has_nul_byte(Z_STR_P(value)))) { + zend_argument_value_error(1, "must not contain any null bytes"); + RETURN_THROWS(); + } + if (UNEXPECTED(Z_STRLEN_P(value) > INT32_MAX)) { + zend_argument_value_error(1, "is too long"); + RETURN_THROWS(); + } + result = NUMBERFORMATTER_OBJECT(nfo)->formatDecimal(icu::StringPiece(Z_STRVAL_P(value), static_cast(Z_STRLEN_P(value))), status).toString(status); + } else if (Z_TYPE_P(value) == IS_DOUBLE) { result = NUMBERFORMATTER_OBJECT(nfo)->formatDouble(Z_DVAL_P(value), status).toString(status); } else { result = NUMBERFORMATTER_OBJECT(nfo)->formatInt(static_cast(Z_LVAL_P(value)), status).toString(status); diff --git a/ext/intl/tests/numberformatter/errors.phpt b/ext/intl/tests/numberformatter/errors.phpt index f04e24b025fc..4ce2d263e152 100644 --- a/ext/intl/tests/numberformatter/errors.phpt +++ b/ext/intl/tests/numberformatter/errors.phpt @@ -21,6 +21,9 @@ var_dump(intl_get_error_message()); var_dump(IntlNumberFormatter::createFromSkeleton('', 'ro_' . str_repeat('a', 160))); var_dump(intl_get_error_message()); +var_dump(IntlNumberFormatter::createFromSkeleton("percent\xFF", 'en_US')); +var_dump(intl_get_error_message()); + $formatter = IntlNumberFormatter::createFromSkeleton('', 'en_US'); var_dump(intl_get_error_code()); @@ -68,9 +71,11 @@ NULL string(107) "IntlNumberFormatter::createFromSkeleton(): Number formatter creation failed: U_NUMBER_SKELETON_SYNTAX_ERROR" NULL string(132) "IntlNumberFormatter::createFromSkeleton(): Locale string too long, should be no longer than 156 characters: U_ILLEGAL_ARGUMENT_ERROR" +NULL +string(100) "IntlNumberFormatter::createFromSkeleton(): Error converting skeleton to UTF-16: U_INVALID_CHAR_FOUND" int(0) IntlException: IntlNumberFormatter::createFromSkeleton(): Number formatter creation failed Error: Call to private IntlNumberFormatter::__construct() from global scope ValueError: IntlNumberFormatter::createFromSkeleton(): Argument #2 ($locale) "invalid-language" is invalid -TypeError: IntlNumberFormatter::format(): Argument #1 ($value) must be of type int|float, array given +TypeError: IntlNumberFormatter::format(): Argument #1 ($value) must be of type string|int|float, array given Error: Trying to clone an uncloneable object of class IntlNumberFormatter From 9a801963e960f19bba06bece50da8874a8d552ef Mon Sep 17 00:00:00 2001 From: David Carlier Date: Sun, 11 Oct 2026 20:29:43 +0100 Subject: [PATCH 3/5] IntlNumberFormatter numeric string and default locale tests. --- UPGRADING | 2 + .../tests/numberformatter/default_locale.phpt | 29 ++++++++++ .../tests/numberformatter/format_string.phpt | 56 +++++++++++++++++++ 3 files changed, 87 insertions(+) create mode 100644 ext/intl/tests/numberformatter/default_locale.phpt create mode 100644 ext/intl/tests/numberformatter/format_string.phpt diff --git a/UPGRADING b/UPGRADING index 07d223c3f6c0..34f90e521bb2 100644 --- a/UPGRADING +++ b/UPGRADING @@ -72,6 +72,8 @@ PHP 8.7 UPGRADE NOTES . Added IntlNumberFormatter class to format a number with a given locale and ICU number skeleton, which also covers measurement units, e.g. "measure-unit/speed-kilometer-per-hour unit-width-full-name". + IntlNumberFormatter::format() accepts an int, a float or a numeric + string, the latter being formatted as a decimal of arbitrary precision. It is supported from icu 63. ======================================== diff --git a/ext/intl/tests/numberformatter/default_locale.phpt b/ext/intl/tests/numberformatter/default_locale.phpt new file mode 100644 index 000000000000..2e6671c70876 --- /dev/null +++ b/ext/intl/tests/numberformatter/default_locale.phpt @@ -0,0 +1,29 @@ +--TEST-- +IntlNumberFormatter::createFromSkeleton() with an empty locale uses the default locale +--EXTENSIONS-- +intl +--SKIPIF-- + +--INI-- +intl.default_locale=en_US +--FILE-- +format(1234.5)); + +Locale::setDefault('de'); + +var_dump(IntlNumberFormatter::createFromSkeleton($skeleton, '')->format(1234.5)); +var_dump(IntlNumberFormatter::createFromSkeleton($skeleton, 'en_US')->format(1234.5)); + +?> +--EXPECT-- +string(27) "1,234.5 kilometers per hour" +string(28) "1.234,5 Kilometer pro Stunde" +string(27) "1,234.5 kilometers per hour" diff --git a/ext/intl/tests/numberformatter/format_string.phpt b/ext/intl/tests/numberformatter/format_string.phpt new file mode 100644 index 000000000000..51ac283c0962 --- /dev/null +++ b/ext/intl/tests/numberformatter/format_string.phpt @@ -0,0 +1,56 @@ +--TEST-- +IntlNumberFormatter::format() with a numeric string +--EXTENSIONS-- +intl +--SKIPIF-- + +--FILE-- +format('12345678901234567890.123456789')); +var_dump($formatter->format('9223372036854775808')); +var_dump($formatter->format('-0.1')); +var_dump($formatter->format('1e3')); + +var_dump(IntlNumberFormatter::createFromSkeleton('', 'en_US')->format('12345678901234567890.123456789')); +var_dump(IntlNumberFormatter::createFromSkeleton('measure-unit/length-meter .00', 'de')->format('1234.5')); + +foreach (['abc', '', ' 12', '1,5'] as $value) { + var_dump($formatter->format($value)); + var_dump($formatter->getErrorMessage()); +} + +var_dump($formatter->format('12')); +var_dump($formatter->getErrorCode()); + +try { + $formatter->format("1\0002"); +} catch (ValueError $e) { + echo $e::class, ': ', $e->getMessage(), PHP_EOL; +} + +?> +--EXPECT-- +string(36) "12,345,678,901,234,567,890.123456789" +string(25) "9,223,372,036,854,775,808" +string(4) "-0.1" +string(5) "1,000" +string(33) "12,345,678,901,234,567,890.123457" +string(10) "1.234,50 m" +bool(false) +string(86) "IntlNumberFormatter::format(): Number formatting failed: U_DECIMAL_NUMBER_SYNTAX_ERROR" +bool(false) +string(86) "IntlNumberFormatter::format(): Number formatting failed: U_DECIMAL_NUMBER_SYNTAX_ERROR" +bool(false) +string(86) "IntlNumberFormatter::format(): Number formatting failed: U_DECIMAL_NUMBER_SYNTAX_ERROR" +bool(false) +string(86) "IntlNumberFormatter::format(): Number formatting failed: U_DECIMAL_NUMBER_SYNTAX_ERROR" +string(2) "12" +int(0) +ValueError: IntlNumberFormatter::format(): Argument #1 ($value) must not contain any null bytes From 15e606f960bb910b143aa21a20171c679648fe0b Mon Sep 17 00:00:00 2001 From: David Carlier Date: Mon, 12 Oct 2026 20:35:09 +0100 Subject: [PATCH 4/5] feedback --- ext/intl/CREDITS | 2 +- .../numberformatter/numberformatter_class.cpp | 2 +- .../locale_posix_modifier.phpt | 21 +++++++++++++++++++ ext/standard/credits_ext.h | 2 +- 4 files changed, 24 insertions(+), 3 deletions(-) create mode 100644 ext/intl/tests/numberformatter/locale_posix_modifier.phpt diff --git a/ext/intl/CREDITS b/ext/intl/CREDITS index 0e7fd55dc12e..02a0f12a139d 100644 --- a/ext/intl/CREDITS +++ b/ext/intl/CREDITS @@ -1,2 +1,2 @@ Internationalization -Ed Batutis, Vladimir Iordanov, Dmitry Lakhtyuk, Stanislav Malyshev, Vadim Savchuk, Kirti Velankar +Ed Batutis, David Carlier, Vladimir Iordanov, Dmitry Lakhtyuk, Stanislav Malyshev, Vadim Savchuk, Kirti Velankar diff --git a/ext/intl/numberformatter/numberformatter_class.cpp b/ext/intl/numberformatter/numberformatter_class.cpp index e59a7f6f9b78..1ed6135907ff 100644 --- a/ext/intl/numberformatter/numberformatter_class.cpp +++ b/ext/intl/numberformatter/numberformatter_class.cpp @@ -102,7 +102,7 @@ U_CFUNC PHP_METHOD(IntlNumberFormatter, createFromSkeleton) INTL_CHECK_STATUS_OR_NULL(status, "Number formatter creation failed"); object_init_ex(return_value, IntlNumberFormatter_ce_ptr); - NUMBERFORMATTER_OBJECT(Z_INTL_INTLNUMBERFORMATTER_P(return_value)) = new LocalizedNumberFormatter(nf.locale(icu::Locale(locale))); + NUMBERFORMATTER_OBJECT(Z_INTL_INTLNUMBERFORMATTER_P(return_value)) = new LocalizedNumberFormatter(nf.locale(icu::Locale::createCanonical(locale))); } U_CFUNC PHP_METHOD(IntlNumberFormatter, format) diff --git a/ext/intl/tests/numberformatter/locale_posix_modifier.phpt b/ext/intl/tests/numberformatter/locale_posix_modifier.phpt new file mode 100644 index 000000000000..89db9664811f --- /dev/null +++ b/ext/intl/tests/numberformatter/locale_posix_modifier.phpt @@ -0,0 +1,21 @@ +--TEST-- +IntlNumberFormatter preserves the region of a locale with a POSIX modifier +--EXTENSIONS-- +intl +--SKIPIF-- + +--FILE-- +format(12345.5) === $modified->format(12345.5)); +?> +--EXPECT-- +bool(true) diff --git a/ext/standard/credits_ext.h b/ext/standard/credits_ext.h index 3e1ba70a71e5..45f8ce89c8d9 100644 --- a/ext/standard/credits_ext.h +++ b/ext/standard/credits_ext.h @@ -31,7 +31,7 @@ CREDIT_LINE("GetText", "Alex Plotnick"); CREDIT_LINE("GNU GMP support", "Stanislav Malyshev"); CREDIT_LINE("Iconv", "Rui Hirokawa, Stig Bakken, Moriyoshi Koizumi"); CREDIT_LINE("Input Filter", "Rasmus Lerdorf, Derick Rethans, Pierre-Alain Joye, Ilia Alshanetsky"); -CREDIT_LINE("Internationalization", "Ed Batutis, Vladimir Iordanov, Dmitry Lakhtyuk, Stanislav Malyshev, Vadim Savchuk, Kirti Velankar"); +CREDIT_LINE("Internationalization", "Ed Batutis, David Carlier, Vladimir Iordanov, Dmitry Lakhtyuk, Stanislav Malyshev, Vadim Savchuk, Kirti Velankar"); CREDIT_LINE("JSON", "Jakub Zelenka, Omar Kilani, Scott MacVicar"); CREDIT_LINE("LDAP", "Amitay Isaacs, Eric Warnke, Rasmus Lerdorf, Gerrit Thomson, Stig Venaas"); CREDIT_LINE("LIBXML", "Christian Stocker, Rob Richards, Marcus Boerger, Wez Furlong, Shane Caraveo"); From 05b80ddc1d05cca579d64519fe4cd7a307863c09 Mon Sep 17 00:00:00 2001 From: David Carlier Date: Tue, 13 Oct 2026 08:54:46 +0100 Subject: [PATCH 5/5] feedback 2 --- ext/intl/numberformatter/numberformatter_class.cpp | 5 ++--- ext/intl/numberformatter/numberformatter_class.h | 3 --- ext/intl/tests/numberformatter/errors.phpt | 5 +++++ 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/ext/intl/numberformatter/numberformatter_class.cpp b/ext/intl/numberformatter/numberformatter_class.cpp index 1ed6135907ff..a7d68993bec4 100644 --- a/ext/intl/numberformatter/numberformatter_class.cpp +++ b/ext/intl/numberformatter/numberformatter_class.cpp @@ -108,14 +108,13 @@ U_CFUNC PHP_METHOD(IntlNumberFormatter, createFromSkeleton) U_CFUNC PHP_METHOD(IntlNumberFormatter, format) { zval *value; - NUMBERFORMATTER_METHOD_INIT_VARS; ZEND_PARSE_PARAMETERS_START(1, 1) Z_PARAM_NUMBER_OR_STR(value) ZEND_PARSE_PARAMETERS_END(); - object = ZEND_THIS; - NUMBERFORMATTER_METHOD_FETCH_OBJECT; + IntlNumberFormatter_object *nfo = Z_INTL_INTLNUMBERFORMATTER_P(ZEND_THIS); + intl_errors_reset(INTL_DATA_ERROR_P(nfo)); UErrorCode status = U_ZERO_ERROR; UnicodeString result; diff --git a/ext/intl/numberformatter/numberformatter_class.h b/ext/intl/numberformatter/numberformatter_class.h index 2215791b9146..79ec5895ac57 100644 --- a/ext/intl/numberformatter/numberformatter_class.h +++ b/ext/intl/numberformatter/numberformatter_class.h @@ -48,9 +48,6 @@ typedef struct { #define NUMBERFORMATTER_OBJECT(nfo) (nfo)->nf_data.unumf -#define NUMBERFORMATTER_METHOD_INIT_VARS INTL_METHOD_INIT_VARS(IntlNumberFormatter, nfo) -#define NUMBERFORMATTER_METHOD_FETCH_OBJECT INTL_METHOD_FETCH_OBJECT(INTL_INTLNUMBERFORMATTER, nfo) - #ifdef __cplusplus extern "C" { #endif diff --git a/ext/intl/tests/numberformatter/errors.phpt b/ext/intl/tests/numberformatter/errors.phpt index 4ce2d263e152..308f29faf5b8 100644 --- a/ext/intl/tests/numberformatter/errors.phpt +++ b/ext/intl/tests/numberformatter/errors.phpt @@ -50,12 +50,16 @@ try { echo $e::class, ': ', $e->getMessage(), PHP_EOL; } +IntlNumberFormatter::createFromSkeleton('invalid skeleton here', 'ro'); + try { $formatter->format([]); } catch (TypeError $e) { echo $e::class, ': ', $e->getMessage(), PHP_EOL; } +var_dump(intl_get_error_code()); + try { clone $formatter; } catch (Error $e) { @@ -78,4 +82,5 @@ IntlException: IntlNumberFormatter::createFromSkeleton(): Number formatter creat Error: Call to private IntlNumberFormatter::__construct() from global scope ValueError: IntlNumberFormatter::createFromSkeleton(): Argument #2 ($locale) "invalid-language" is invalid TypeError: IntlNumberFormatter::format(): Argument #1 ($value) must be of type string|int|float, array given +int(65811) Error: Trying to clone an uncloneable object of class IntlNumberFormatter