diff --git a/ext/intl/config.m4 b/ext/intl/config.m4 index 6a64f0f718100..20adc3a4ce3a7 100644 --- a/ext/intl/config.m4 +++ b/ext/intl/config.m4 @@ -39,6 +39,7 @@ if test "$PHP_INTL" != "no"; then locale/locale_class.c locale/locale_methods.c locale/locale.c + listformatter/listformatter_class.c msgformat/msgformat_attr.c msgformat/msgformat_class.c msgformat/msgformat_data.c @@ -119,6 +120,7 @@ if test "$PHP_INTL" != "no"; then $ext_builddir/grapheme $ext_builddir/idn $ext_builddir/locale + $ext_builddir/listformatter $ext_builddir/msgformat $ext_builddir/normalizer $ext_builddir/resourcebundle diff --git a/ext/intl/config.w32 b/ext/intl/config.w32 index 17b577327bbb9..b8161865d2540 100644 --- a/ext/intl/config.w32 +++ b/ext/intl/config.w32 @@ -39,6 +39,9 @@ if (PHP_INTL != "no") { formatter_main.c \ formatter_parse.c \ ", "intl"); + ADD_SOURCES(configure_module_dirname + "/listformatter", "\ + listformatter_class.c \ + ", "intl"); ADD_SOURCES(configure_module_dirname + "/locale", "\ locale.c \ locale_class.c \ diff --git a/ext/intl/listformatter/listformatter.stub.php b/ext/intl/listformatter/listformatter.stub.php new file mode 100644 index 0000000000000..28a37f3c5bf0f --- /dev/null +++ b/ext/intl/listformatter/listformatter.stub.php @@ -0,0 +1,36 @@ + | + +----------------------------------------------------------------------+ +*/ + +#include "php.h" +#include "php_intl.h" +#include +#include "listformatter_arginfo.h" +#include "listformatter_class.h" +#include "intl_convert.h" + +static zend_object_handlers listformatter_handlers; + +/* {{{ listformatter_free_obj */ +static void listformatter_free_obj(zend_object *object) +{ + ListFormatter_object *obj = php_intl_listformatter_fetch_object(object); + + if( obj->lf_data.ulistfmt ) + ulistfmt_close( obj->lf_data.ulistfmt ); + + obj->lf_data.ulistfmt = NULL; + intl_error_reset( &obj->lf_data.error ); + + zend_object_std_dtor(&obj->zo); +} +/* }}} */ + +/* {{{ listformatter_create_object */ +static zend_object *listformatter_create_object(zend_class_entry *class_type) +{ + ListFormatter_object *obj; + obj = zend_object_alloc(sizeof(ListFormatter_object), class_type); + + obj->lf_data.ulistfmt = NULL; + intl_error_reset( &obj->lf_data.error ); + + zend_object_std_init(&obj->zo, class_type); + object_properties_init(&obj->zo, class_type); + obj->zo.handlers = &listformatter_handlers; + return &obj->zo; +} +/* }}} */ + +/* {{{ listformatter_create_object */ +PHP_METHOD(IntlListFormatter, __construct) +{ + ListFormatter_object *obj = Z_INTL_LISTFORMATTER_P(ZEND_THIS); + char* locale; + size_t locale_len = 0; + zend_long type = ULISTFMT_TYPE_AND; + zend_long width = ULISTFMT_WIDTH_WIDE; + ZEND_PARSE_PARAMETERS_START(1, 3) + Z_PARAM_STRING(locale, locale_len) + Z_PARAM_OPTIONAL + Z_PARAM_LONG(type) + Z_PARAM_LONG(width) + ZEND_PARSE_PARAMETERS_END(); + + if(locale_len == 0) { + locale = (char *)intl_locale_get_default(); + } + + if (locale_len > INTL_MAX_LOCALE_LEN) { + zend_argument_value_error(1, "Locale string too long, should be no longer than %d characters", INTL_MAX_LOCALE_LEN); + RETURN_THROWS(); + } + + if (strlen(uloc_getISO3Language(locale)) == 0) { + zend_argument_value_error(1, "\"%s\" is invalid", locale); + RETURN_THROWS(); + } + + UErrorCode status = U_ZERO_ERROR; + #if U_ICU_VERSION_MAJOR_NUM >= 67 + if (type != ULISTFMT_TYPE_AND && type != ULISTFMT_TYPE_OR && type != ULISTFMT_TYPE_UNITS) { + zend_argument_value_error(2, "must be one of IntlListFormatter::TYPE_AND, IntlListFormatter::TYPE_OR, or IntlListFormatter::TYPE_UNITS"); + RETURN_THROWS(); + } + + if (width != ULISTFMT_WIDTH_WIDE && width != ULISTFMT_WIDTH_SHORT && width != ULISTFMT_WIDTH_NARROW) { + zend_argument_value_error(3, "must be one of IntlListFormatter::WIDTH_WIDE, IntlListFormatter::WIDTH_SHORT, or IntlListFormatter::WIDTH_NARROW"); + RETURN_THROWS(); + } + + LISTFORMATTER_OBJECT(obj) = ulistfmt_openForType(locale, type, width, &status); + #else + if (type != ULISTFMT_TYPE_AND) { + zend_argument_value_error(2, "ICU 66 and below only support IntlListFormatter::TYPE_AND"); + RETURN_THROWS(); + } + + if (width != ULISTFMT_WIDTH_WIDE) { + zend_argument_value_error(3, "ICU 66 and below only support IntlListFormatter::WIDTH_WIDE"); + RETURN_THROWS(); + } + + LISTFORMATTER_OBJECT(obj) = ulistfmt_open(locale, &status); + #endif + + if (U_FAILURE(status)) { + intl_error_set(NULL, status, "Constructor failed", 0); + zend_throw_exception(IntlException_ce_ptr, "Constructor failed", 0); + RETURN_THROWS(); + } +} +/* }}} */ + +/* {{{ listformatter_format */ +PHP_METHOD(IntlListFormatter, format) +{ + ListFormatter_object *obj = Z_INTL_LISTFORMATTER_P(ZEND_THIS); + zval *strings; + + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_ARRAY(strings) + ZEND_PARSE_PARAMETERS_END(); + + if (!LISTFORMATTER_OBJECT(obj)) { + intl_error_set(NULL, U_ILLEGAL_ARGUMENT_ERROR, "ListFormatter not properly constructed", 0); + RETURN_FALSE; + } + + HashTable *ht = Z_ARRVAL_P(strings); + uint32_t count = zend_array_count(ht); + if (count == 0) { + RETURN_EMPTY_STRING(); + } + + const UChar **items = (const UChar **)safe_emalloc(count, sizeof(const UChar *), 0); + int32_t *itemLengths = (int32_t *)safe_emalloc(count, sizeof(int32_t), 0); + uint32_t i = 0; + zval *val; + + ZEND_HASH_FOREACH_VAL(ht, val) { + zend_string *str_val; + + str_val = zval_get_string(val); + + // Convert PHP string to UTF-16 + UChar *ustr = NULL; + int32_t ustr_len = 0; + UErrorCode status = U_ZERO_ERROR; + + intl_convert_utf8_to_utf16(&ustr, &ustr_len, ZSTR_VAL(str_val), ZSTR_LEN(str_val), &status); + zend_string_release(str_val); + + if (U_FAILURE(status)) { + // We can't use goto cleanup because items and itemLengths are incompletely allocated + for (uint32_t j = 0; j < i; j++) { + efree((void *)items[j]); + } + efree(items); + efree(itemLengths); + intl_error_set(NULL, status, "Failed to convert string to UTF-16", 0); + RETURN_FALSE; + } + + items[i] = ustr; + itemLengths[i] = ustr_len; + i++; + } ZEND_HASH_FOREACH_END(); + + UErrorCode status = U_ZERO_ERROR; + int32_t resultLength; + UChar *result = NULL; + + resultLength = ulistfmt_format(LISTFORMATTER_OBJECT(obj), items, itemLengths, count, NULL, 0, &status); + + if (U_FAILURE(status) && status != U_BUFFER_OVERFLOW_ERROR) { + intl_error_set(NULL, status, "Failed to format list", 0); + RETVAL_FALSE; + goto cleanup; + } + + // Allocate buffer and try again + status = U_ZERO_ERROR; + result = (UChar *)safe_emalloc(resultLength + 1, sizeof(UChar), 0); + ulistfmt_format(LISTFORMATTER_OBJECT(obj), items, itemLengths, count, result, resultLength, &status); + + if (U_FAILURE(status)) { + if (result) { + efree(result); + } + intl_error_set(NULL, status, "Failed to format list", 0); + RETVAL_FALSE; + goto cleanup; + } + + // Convert result back to UTF-8 + zend_string *ret = intl_convert_utf16_to_utf8(result, resultLength, &status); + efree(result); + + if (!ret) { + intl_error_set(NULL, status, "Failed to convert result to UTF-8", 0); + RETVAL_FALSE; + } else { + RETVAL_NEW_STR(ret); + } + + cleanup: + for (i = 0; i < count; i++) { + efree((void *)items[i]); + } + efree(items); + efree(itemLengths); +} +/* }}} */ + +/* {{{ listformatter_getErrorCode */ +PHP_METHOD(IntlListFormatter, getErrorCode) +{ + ZEND_PARSE_PARAMETERS_NONE(); + + ListFormatter_object *obj = Z_INTL_LISTFORMATTER_P(ZEND_THIS); + + UErrorCode status = intl_error_get_code(LISTFORMATTER_ERROR_P(obj)); + + RETURN_LONG(status); +} +/* }}} */ + +/* {{{ listformatter_getErrorMessage */ +PHP_METHOD(IntlListFormatter, getErrorMessage) +{ + ZEND_PARSE_PARAMETERS_NONE(); + + ListFormatter_object *obj = Z_INTL_LISTFORMATTER_P(ZEND_THIS); + + zend_string *message = intl_error_get_message(LISTFORMATTER_ERROR_P(obj)); + RETURN_STR(message); +} +/* }}} */ + +/* {{{ listformatter_register_class */ +void listformatter_register_class(void) +{ + zend_class_entry *class_entry = register_class_IntlListFormatter(); + class_entry->create_object = listformatter_create_object; + + memcpy(&listformatter_handlers, zend_get_std_object_handlers(), sizeof(zend_object_handlers)); + listformatter_handlers.offset = XtOffsetOf(ListFormatter_object, zo); + listformatter_handlers.free_obj = listformatter_free_obj; +} +/* }}} */ diff --git a/ext/intl/listformatter/listformatter_class.h b/ext/intl/listformatter/listformatter_class.h new file mode 100644 index 0000000000000..4097b29dc631c --- /dev/null +++ b/ext/intl/listformatter/listformatter_class.h @@ -0,0 +1,52 @@ +/* + +----------------------------------------------------------------------+ + | This source file is subject to version 3.01 of the PHP license, | + | that is bundled with this package in the file LICENSE, and is | + | available through the world-wide-web at the following url: | + | https://www.php.net/license/3_01.txt | + | If you did not receive a copy of the PHP license and are unable to | + | obtain it through the world-wide-web, please send a note to | + | license@php.net so we can mail you a copy immediately. | + +----------------------------------------------------------------------+ + | Authors: Bogdan Ungureanu | + +----------------------------------------------------------------------+ +*/ + +#ifndef LISTFORMATTER_CLASS_H +#define LISTFORMATTER_CLASS_H + +#include + +#include "intl_common.h" +#include "intl_error.h" +#include "intl_data.h" + +#include + +typedef struct { + // error handling + intl_error error; + + // formatter handling + UListFormatter* ulistfmt; +} listformatter_data; + +typedef struct { + listformatter_data lf_data; + zend_object zo; +} ListFormatter_object; + +static inline ListFormatter_object *php_intl_listformatter_fetch_object(zend_object *obj) { + return (ListFormatter_object *)((char*)(obj) - XtOffsetOf(ListFormatter_object, zo)); +} +#define Z_INTL_LISTFORMATTER_P(zv) php_intl_listformatter_fetch_object(Z_OBJ_P(zv)) + +#define LISTFORMATTER_ERROR(lfo) (lfo)->lf_data.error +#define LISTFORMATTER_ERROR_P(lfo) &(LISTFORMATTER_ERROR(lfo)) + +#define LISTFORMATTER_OBJECT(lfo) (lfo)->lf_data.ulistfmt + +void listformatter_register_class( void ); +extern zend_class_entry *ListFormatter_ce_ptr; + +#endif // LISTFORMATTER_CLASS_H \ No newline at end of file diff --git a/ext/intl/php_intl.c b/ext/intl/php_intl.c index cba18f5ae07b1..68fd2dedfba85 100644 --- a/ext/intl/php_intl.c +++ b/ext/intl/php_intl.c @@ -41,6 +41,8 @@ #include "locale/locale.h" #include "locale/locale_class.h" +#include "listformatter/listformatter_class.h" + #include "dateformat/dateformat.h" #include "dateformat/dateformat_class.h" #include "dateformat/dateformat_data.h" @@ -156,6 +158,9 @@ PHP_MINIT_FUNCTION( intl ) /* Register 'NumberFormatter' PHP class */ formatter_register_class( ); + /* Register 'ListFormatter' PHP class */ + listformatter_register_class( ); + /* Register 'Normalizer' PHP class */ normalizer_register_Normalizer_class( ); diff --git a/ext/intl/tests/listformatter/listformatter_basic.phpt b/ext/intl/tests/listformatter/listformatter_basic.phpt new file mode 100644 index 0000000000000..07abb2ac5da0f --- /dev/null +++ b/ext/intl/tests/listformatter/listformatter_basic.phpt @@ -0,0 +1,61 @@ +--TEST-- +IntlListFormatter: Basic functionality +--EXTENSIONS-- +intl +--FILE-- +format([1,2,3]) . PHP_EOL; +$formatter = new IntlListFormatter('EN_US'); +echo $formatter->format([1,2,3]) . PHP_EOL; + +echo $formatter->format([1.2,2.3,3.4]) . PHP_EOL; + +$item = 'test'; +$item2 = 'test2'; +$item3 = &$item; +$items = [$item, $item2, $item3]; +$items2 = &$items; + +echo $formatter->format($items) . PHP_EOL; +echo $formatter->format($items2) . PHP_EOL; + +echo $formatter->format([null, true, false]) . PHP_EOL; + +$classItem = new class { + public function __toString() { + return 'foo'; + } +}; + +echo $formatter->format([1, $classItem]) . PHP_EOL; + + +echo 'FR' . PHP_EOL; + +$formatter = new IntlListFormatter('FR', IntlListFormatter::TYPE_AND, IntlListFormatter::WIDTH_WIDE); +echo $formatter->format([1,2,3]) . PHP_EOL; + +$formatter = new IntlListFormatter('FR'); +echo $formatter->format([1,2,3]) . PHP_EOL; + +// Make it clear that numbers are not converted automatically to the locale. Use NumberFormatter for each value. +echo $formatter->format([1.2,2.3,3.4]) . PHP_EOL; + + +--EXPECT-- +EN_US +1, 2, and 3 +1, 2, and 3 +1.2, 2.3, and 3.4 +test, test2, and test +test, test2, and test +, 1, and +1 and foo +FR +1, 2 et 3 +1, 2 et 3 +1.2, 2.3 et 3.4 \ No newline at end of file diff --git a/ext/intl/tests/listformatter/listformatter_error.phpt b/ext/intl/tests/listformatter/listformatter_error.phpt new file mode 100644 index 0000000000000..0709c98313c37 --- /dev/null +++ b/ext/intl/tests/listformatter/listformatter_error.phpt @@ -0,0 +1,38 @@ +--TEST-- +IntlListFormatter: error messages +--EXTENSIONS-- +intl +--FILE-- +getMessage() . PHP_EOL; +} + +try { + $formatter = new IntlListFormatter('ro_thisiswaytooooooooooooooooooooooooooooooooooooooooooooolongtobevaliditneedstobeatleast157characterstofailthevalidationinthelistformattercodeimplementation', IntlListFormatter::TYPE_AND, IntlListFormatter::WIDTH_WIDE); +} catch(ValueError $exception) { + echo $exception->getMessage() . PHP_EOL; +} + +$formatter = new IntlListFormatter('ro', IntlListFormatter::TYPE_AND, IntlListFormatter::WIDTH_WIDE); + +try { + echo $formatter->format([new stdClass()]); +} catch(Error $error) { + echo $error->getMessage() . PHP_EOL; +} + +try { + echo $formatter->format([1, 2, new stdClass(), 4]); +} catch(Error $error) { + echo $error->getMessage() . PHP_EOL; +} + +--EXPECT-- +IntlListFormatter::__construct(): Argument #1 ($locale) "f" is invalid +IntlListFormatter::__construct(): Argument #1 ($locale) Locale string too long, should be no longer than 156 characters +Object of class stdClass could not be converted to string +Object of class stdClass could not be converted to string \ No newline at end of file diff --git a/ext/intl/tests/listformatter/listformatter_with_paramaters.phpt b/ext/intl/tests/listformatter/listformatter_with_paramaters.phpt new file mode 100644 index 0000000000000..0c56b57cfdc94 --- /dev/null +++ b/ext/intl/tests/listformatter/listformatter_with_paramaters.phpt @@ -0,0 +1,126 @@ +--TEST-- +IntlListFormatter: Test AND, OR and Width parameters +--EXTENSIONS-- +intl +--SKIPIF-- + +--FILE-- +format([1,2,3]) . PHP_EOL; + +$formatter = new IntlListFormatter('EN_US', IntlListFormatter::TYPE_AND, IntlListFormatter::WIDTH_SHORT); +echo $formatter->format([1,2,3]) . PHP_EOL; + +$formatter = new IntlListFormatter('EN_US', IntlListFormatter::TYPE_AND, IntlListFormatter::WIDTH_NARROW); +echo $formatter->format([1,2,3]) . PHP_EOL; + +$formatter = new IntlListFormatter('EN_US', IntlListFormatter::TYPE_OR, IntlListFormatter::WIDTH_WIDE); +echo $formatter->format([1,2,3]) . PHP_EOL; + +$formatter = new IntlListFormatter('EN_US', IntlListFormatter::TYPE_OR, IntlListFormatter::WIDTH_SHORT); +echo $formatter->format([1,2,3]) . PHP_EOL; + +$formatter = new IntlListFormatter('EN_US', IntlListFormatter::TYPE_OR, IntlListFormatter::WIDTH_NARROW); +echo $formatter->format([1,2,3]) . PHP_EOL; + +$formatter = new IntlListFormatter('EN_US', IntlListFormatter::TYPE_UNITS, IntlListFormatter::WIDTH_WIDE); +echo $formatter->format([1,2,3]) . PHP_EOL; + +$formatter = new IntlListFormatter('EN_US', IntlListFormatter::TYPE_UNITS, IntlListFormatter::WIDTH_SHORT); +echo $formatter->format([1,2,3]) . PHP_EOL; + +$formatter = new IntlListFormatter('EN_US', IntlListFormatter::TYPE_UNITS, IntlListFormatter::WIDTH_NARROW); +echo $formatter->format([1,2,3]) . PHP_EOL; + +echo 'GB' . PHP_EOL; + +$formatter = new IntlListFormatter('en_GB', IntlListFormatter::TYPE_AND, IntlListFormatter::WIDTH_SHORT); +echo $formatter->format([1,2,3]) . PHP_EOL; + +$formatter = new IntlListFormatter('en_GB', IntlListFormatter::TYPE_AND, IntlListFormatter::WIDTH_SHORT); +echo $formatter->format([1,2,3]) . PHP_EOL; + +$formatter = new IntlListFormatter('en_GB', IntlListFormatter::TYPE_AND, IntlListFormatter::WIDTH_NARROW); +echo $formatter->format([1,2,3]) . PHP_EOL; + +$formatter = new IntlListFormatter('en_GB', IntlListFormatter::TYPE_OR, IntlListFormatter::WIDTH_WIDE); +echo $formatter->format([1,2,3]) . PHP_EOL; + +$formatter = new IntlListFormatter('en_GB', IntlListFormatter::TYPE_OR, IntlListFormatter::WIDTH_SHORT); +echo $formatter->format([1,2,3]) . PHP_EOL; + +$formatter = new IntlListFormatter('en_GB', IntlListFormatter::TYPE_OR, IntlListFormatter::WIDTH_NARROW); +echo $formatter->format([1,2,3]) . PHP_EOL; + +$formatter = new IntlListFormatter('en_GB', IntlListFormatter::TYPE_UNITS, IntlListFormatter::WIDTH_WIDE); +echo $formatter->format([1,2,3]) . PHP_EOL; + +$formatter = new IntlListFormatter('en_GB', IntlListFormatter::TYPE_UNITS, IntlListFormatter::WIDTH_SHORT); +echo $formatter->format([1,2,3]) . PHP_EOL; + +$formatter = new IntlListFormatter('en_GB', IntlListFormatter::TYPE_UNITS, IntlListFormatter::WIDTH_NARROW); +echo $formatter->format([1,2,3]) . PHP_EOL; + +echo 'FR' . PHP_EOL; + +$formatter = new IntlListFormatter('FR', IntlListFormatter::TYPE_AND, IntlListFormatter::WIDTH_SHORT); +echo $formatter->format([1,2,3]) . PHP_EOL; + +$formatter = new IntlListFormatter('FR', IntlListFormatter::TYPE_AND, IntlListFormatter::WIDTH_SHORT); +echo $formatter->format([1,2,3]) . PHP_EOL; + +$formatter = new IntlListFormatter('FR', IntlListFormatter::TYPE_AND, IntlListFormatter::WIDTH_NARROW); +echo $formatter->format([1,2,3]) . PHP_EOL; + +$formatter = new IntlListFormatter('FR', IntlListFormatter::TYPE_OR, IntlListFormatter::WIDTH_WIDE); +echo $formatter->format([1,2,3]) . PHP_EOL; + +$formatter = new IntlListFormatter('FR', IntlListFormatter::TYPE_OR, IntlListFormatter::WIDTH_SHORT); +echo $formatter->format([1,2,3]) . PHP_EOL; + +$formatter = new IntlListFormatter('FR', IntlListFormatter::TYPE_OR, IntlListFormatter::WIDTH_NARROW); +echo $formatter->format([1,2,3]) . PHP_EOL; + +$formatter = new IntlListFormatter('FR', IntlListFormatter::TYPE_UNITS, IntlListFormatter::WIDTH_WIDE); +echo $formatter->format([1,2,3]) . PHP_EOL; + +$formatter = new IntlListFormatter('FR', IntlListFormatter::TYPE_UNITS, IntlListFormatter::WIDTH_SHORT); +echo $formatter->format([1,2,3]) . PHP_EOL; + +$formatter = new IntlListFormatter('FR', IntlListFormatter::TYPE_UNITS, IntlListFormatter::WIDTH_NARROW); +echo $formatter->format([1,2,3]); +--EXPECT-- +EN_US +1, 2, and 3 +1, 2, & 3 +1, 2, 3 +1, 2, or 3 +1, 2, or 3 +1, 2, or 3 +1, 2, 3 +1, 2, 3 +1 2 3 +GB +1, 2 and 3 +1, 2 and 3 +1, 2, 3 +1, 2 or 3 +1, 2 or 3 +1, 2 or 3 +1, 2, 3 +1, 2, 3 +1 2 3 +FR +1, 2 et 3 +1, 2 et 3 +1, 2, 3 +1, 2 ou 3 +1, 2 ou 3 +1, 2 ou 3 +1, 2 et 3 +1, 2 et 3 +1 2 3 \ No newline at end of file diff --git a/ext/intl/tests/listformatter/listformatter_with_parameters_error.phpt b/ext/intl/tests/listformatter/listformatter_with_parameters_error.phpt new file mode 100644 index 0000000000000..660f76d32466e --- /dev/null +++ b/ext/intl/tests/listformatter/listformatter_with_parameters_error.phpt @@ -0,0 +1,25 @@ +--TEST-- +IntlListFormatter: Test invalid parameters for TYPE and WIDTH +--EXTENSIONS-- +intl +--SKIPIF-- + +--FILE-- +getMessage(); +} + +echo PHP_EOL; + +try { + $formatter = new IntlListFormatter('ro', IntlListFormatter::TYPE_AND, 2323232); +} catch(ValueError $exception) { + echo $exception->getMessage(); +} +--EXPECT-- +IntlListFormatter::__construct(): Argument #2 ($type) must be one of IntlListFormatter::TYPE_AND, IntlListFormatter::TYPE_OR, or IntlListFormatter::TYPE_UNITS +IntlListFormatter::__construct(): Argument #3 ($width) must be one of IntlListFormatter::WIDTH_WIDE, IntlListFormatter::WIDTH_SHORT, or IntlListFormatter::WIDTH_NARROW