Commit 958bcb00637 for php.net

commit 958bcb00637dceb598d4216242a4007a0f68d260
Author: Weilin Du <weilindu@php.net>
Date:   Sun Jun 28 13:29:19 2026 +0800

    ext/Intl: Implement a compatibility layer for ICU version-specific API difference (#22446)

    It is quite annoying to deal with ICU version guards here and there throughout
    the whole extension. However, the guards are on the other hand important for
    the extension to work as we must check for ICU compatibility for some of our
    features. But that doesn't mean we need to write all these shitty #if stuff
    everywhere.

    I therefore wrote this PR to implement several internal APIs to check for
    compatibility. This very much enhance readability and reduce the possibility
    of making mistakes in the future when we are adding guards to new stuffs.

diff --git a/ext/intl/config.m4 b/ext/intl/config.m4
index 7251f228353..62ea18c4470 100644
--- a/ext/intl/config.m4
+++ b/ext/intl/config.m4
@@ -9,6 +9,7 @@ if test "$PHP_INTL" != "no"; then
   INTL_COMMON_FLAGS="$ICU_CFLAGS -Wno-write-strings -D__STDC_LIMIT_MACROS -D__STDC_CONSTANT_MACROS -D__STDC_FORMAT_MACROS -DZEND_ENABLE_STATIC_TSRMLS_CACHE=1"
   PHP_NEW_EXTENSION([intl], m4_normalize([
       intl_convert.c
+      intl_icu_compat.c
       intl_error.c
       php_intl.c
     ]),
diff --git a/ext/intl/config.w32 b/ext/intl/config.w32
index 9e2f695532d..016f17835f8 100644
--- a/ext/intl/config.w32
+++ b/ext/intl/config.w32
@@ -9,7 +9,7 @@ if (PHP_INTL != "no") {
 		CHECK_LIB("icuuc.lib", "intl", PHP_INTL) &&
 					CHECK_HEADER("unicode/utf.h", "CFLAGS_INTL")) {
 		// always build as shared - zend_strtod.c/ICU type conflict
-		EXTENSION("intl", "php_intl.c intl_convert.c intl_convertcpp.cpp intl_error.c ", true,
+		EXTENSION("intl", "php_intl.c intl_convert.c intl_icu_compat.c intl_convertcpp.cpp intl_error.c ", true,
 								"/I \"" + configure_module_dirname + "\" /DZEND_ENABLE_STATIC_TSRMLS_CACHE=1");
 		ADD_EXTENSION_DEP('intl', 'date');
 		ADD_SOURCES(configure_module_dirname + "/collator", "\
diff --git a/ext/intl/converter/converter.cpp b/ext/intl/converter/converter.cpp
index 30855d1b0c5..b4d62d4643d 100644
--- a/ext/intl/converter/converter.cpp
+++ b/ext/intl/converter/converter.cpp
@@ -20,6 +20,8 @@
 #include <unicode/ucnv.h>
 #include <unicode/ustring.h>

+#include "../intl_icu_compat.h"
+
 extern "C" {
 #include "converter.h"
 #include "php_intl.h"
@@ -949,18 +951,10 @@ static zend_object *php_converter_clone_object(zend_object *object) {
 	zend_object *retval = php_converter_object_ctor(object->ce, &objval);
 	UErrorCode error = U_ZERO_ERROR;

-#if U_ICU_VERSION_MAJOR_NUM > 70
-	objval->src = ucnv_clone(oldobj->src, &error);
-#else
-	objval->src = ucnv_safeClone(oldobj->src, NULL, NULL, &error);
-#endif
+	objval->src = intl_icu_compat_ucnv_clone(oldobj->src, &error);
 	if (U_SUCCESS(error)) {
 		error = U_ZERO_ERROR;
-#if U_ICU_VERSION_MAJOR_NUM > 70
-		objval->dest = ucnv_clone(oldobj->dest, &error);
-#else
-		objval->dest = ucnv_safeClone(oldobj->dest, NULL, NULL, &error);
-#endif
+		objval->dest = intl_icu_compat_ucnv_clone(oldobj->dest, &error);
 	}

 	if (U_FAILURE(error)) {
diff --git a/ext/intl/grapheme/grapheme_string.cpp b/ext/intl/grapheme/grapheme_string.cpp
index 9b463fcd94c..eb8b7e609b8 100644
--- a/ext/intl/grapheme/grapheme_string.cpp
+++ b/ext/intl/grapheme/grapheme_string.cpp
@@ -345,7 +345,6 @@ U_CFUNC PHP_FUNCTION(grapheme_substr)
 	int32_t start = 0;
 	int iter_val;
 	UErrorCode status;
-	unsigned char u_break_iterator_buffer[U_BRK_SAFECLONE_BUFFERSIZE];
 	UBreakIterator* bi = nullptr;
 	int sub_str_start_pos, sub_str_end_pos;
 	int32_t (*iter_func)(UBreakIterator *);
@@ -407,7 +406,7 @@ U_CFUNC PHP_FUNCTION(grapheme_substr)
 		RETURN_FALSE;
 	}

-	bi = grapheme_get_break_iterator((void*)u_break_iterator_buffer, &status );
+	bi = grapheme_get_break_iterator(&status);

 	if( U_FAILURE(status) ) {
 		RETURN_FALSE;
@@ -729,7 +728,6 @@ U_CFUNC PHP_FUNCTION(grapheme_extract)
 	int32_t start = 0;
 	zend_long extract_type = GRAPHEME_EXTRACT_TYPE_COUNT;
 	UErrorCode status;
-	unsigned char u_break_iterator_buffer[U_BRK_SAFECLONE_BUFFERSIZE];
 	UBreakIterator* bi = nullptr;
 	int ret_pos;
 	zval *next = nullptr; /* return offset of next part of the string */
@@ -829,7 +827,7 @@ U_CFUNC PHP_FUNCTION(grapheme_extract)

 	bi = nullptr;
 	status = U_ZERO_ERROR;
-	bi = grapheme_get_break_iterator(u_break_iterator_buffer, &status );
+	bi = grapheme_get_break_iterator(&status);

 	ubrk_setUText(bi, &ut, &status);
 	/* if the caller put us in the middle of a grapheme, we can't detect it in all cases since we
@@ -855,7 +853,6 @@ U_CFUNC PHP_FUNCTION(grapheme_str_split)
 	zend_string *str;
 	zend_long split_len = 1;

-	unsigned char u_break_iterator_buffer[U_BRK_SAFECLONE_BUFFERSIZE];
 	UErrorCode ustatus = U_ZERO_ERROR;
 	int32_t pos, current, i, end_len = 0;
 	UBreakIterator* bi;
@@ -891,7 +888,7 @@ U_CFUNC PHP_FUNCTION(grapheme_str_split)

 	bi = nullptr;
 	ustatus = U_ZERO_ERROR;
-	bi = grapheme_get_break_iterator((void*)u_break_iterator_buffer, &ustatus );
+	bi = grapheme_get_break_iterator(&ustatus);

 	if( U_FAILURE(ustatus) ) {
 		RETURN_FALSE;
@@ -1031,9 +1028,7 @@ U_CFUNC PHP_FUNCTION(grapheme_levenshtein)
 		goto out_ustring2;
 	}

-	unsigned char u_break_iterator_buffer1[U_BRK_SAFECLONE_BUFFERSIZE];
-	unsigned char u_break_iterator_buffer2[U_BRK_SAFECLONE_BUFFERSIZE];
-	bi1 = grapheme_get_break_iterator(u_break_iterator_buffer1, &ustatus);
+	bi1 = grapheme_get_break_iterator(&ustatus);
 	if (U_FAILURE(ustatus)) {
 		intl_error_set_code(NULL, ustatus);
 		intl_error_set_custom_msg(NULL, "Error on grapheme_get_break_iterator for argument #1 ($string1)");
@@ -1041,7 +1036,7 @@ U_CFUNC PHP_FUNCTION(grapheme_levenshtein)
 		goto out_bi1;
 	}

-	bi2 = grapheme_get_break_iterator(u_break_iterator_buffer2, &ustatus);
+	bi2 = grapheme_get_break_iterator(&ustatus);
 	if (U_FAILURE(ustatus)) {
 		intl_error_set_code(NULL, ustatus);
 		intl_error_set_custom_msg(NULL, "Error on grapheme_get_break_iterator for argument #2 ($string2)");
@@ -1144,7 +1139,6 @@ U_CFUNC PHP_FUNCTION(grapheme_strrev)
 	char *pstr, *end, *p;
 	zend_string *ret;
 	int32_t pos = 0, current = 0, end_len = 0;
-	unsigned char u_break_iterator_buffer[U_BRK_SAFECLONE_BUFFERSIZE];

 	ZEND_PARSE_PARAMETERS_START(1, 1)
 		Z_PARAM_STR(string)
@@ -1168,7 +1162,7 @@ U_CFUNC PHP_FUNCTION(grapheme_strrev)
 	bi = nullptr;
 	ustatus = U_ZERO_ERROR;

-	bi = grapheme_get_break_iterator((void*)u_break_iterator_buffer, &ustatus );
+	bi = grapheme_get_break_iterator(&ustatus);
 	ret = zend_string_alloc(ZSTR_LEN(string), 0);
 	p = ZSTR_VAL(ret);

diff --git a/ext/intl/grapheme/grapheme_util.cpp b/ext/intl/grapheme/grapheme_util.cpp
index d5bbef6009c..1e6fb94c940 100644
--- a/ext/intl/grapheme/grapheme_util.cpp
+++ b/ext/intl/grapheme/grapheme_util.cpp
@@ -35,6 +35,8 @@ extern "C" {
 #include <unicode/ubrk.h>
 #include <unicode/usearch.h>

+#include "../intl_icu_compat.h"
+
 ZEND_EXTERN_MODULE_GLOBALS( intl )

 /* }}} */
@@ -105,7 +107,6 @@ U_CFUNC int32_t grapheme_strpos_utf16(char *haystack, size_t haystack_len, char
 {
 	UChar *uhaystack = NULL, *uneedle = NULL;
 	int32_t uhaystack_len = 0, uneedle_len = 0, char_pos, ret_pos, offset_pos = 0;
-	unsigned char u_break_iterator_buffer[U_BRK_SAFECLONE_BUFFERSIZE];
 	UBreakIterator* bi = NULL;
 	UErrorCode status;
 	UStringSearch* src = NULL;
@@ -125,7 +126,7 @@ U_CFUNC int32_t grapheme_strpos_utf16(char *haystack, size_t haystack_len, char

 	/* get a pointer to the haystack taking into account the offset */
 	status = U_ZERO_ERROR;
-	bi = grapheme_get_break_iterator(u_break_iterator_buffer, &status );
+	bi = grapheme_get_break_iterator(&status);
 	STRPOS_CHECK_STATUS(status, "Failed to get iterator");
 	status = U_ZERO_ERROR;
 	ubrk_setText(bi, uhaystack, uhaystack_len, &status);
@@ -235,12 +236,11 @@ U_CFUNC zend_long grapheme_ascii_check(const unsigned char *day, size_t len)
 /* {{{ grapheme_split_string: find and optionally return grapheme boundaries */
 U_CFUNC int32_t grapheme_split_string(const UChar *text, int32_t text_length, int boundary_array[], int boundary_array_len )
 {
-	unsigned char u_break_iterator_buffer[U_BRK_SAFECLONE_BUFFERSIZE];
 	UErrorCode		status = U_ZERO_ERROR;
 	int ret_len, pos;
 	UBreakIterator* bi;

-	bi = grapheme_get_break_iterator((void*)u_break_iterator_buffer, &status );
+	bi = grapheme_get_break_iterator(&status);

 	if( U_FAILURE(status) ) {
 		return -1;
@@ -375,7 +375,7 @@ U_CFUNC zend_long grapheme_strrpos_ascii(char *haystack, size_t haystack_len, ch
 /* }}} */

 /* {{{ grapheme_get_break_iterator: get a clone of the global character break iterator */
-U_CFUNC UBreakIterator* grapheme_get_break_iterator(void *stack_buffer, UErrorCode *status )
+U_CFUNC UBreakIterator* grapheme_get_break_iterator(UErrorCode *status )
 {
 	UBreakIterator *global_break_iterator = INTL_G( grapheme_iterator );

@@ -390,12 +390,6 @@ U_CFUNC UBreakIterator* grapheme_get_break_iterator(void *stack_buffer, UErrorCo
 		INTL_G(grapheme_iterator) = global_break_iterator;
 	}

-#if U_ICU_VERSION_MAJOR_NUM >= 69
-	return ubrk_clone(global_break_iterator, status);
-#else
-	int32_t buffer_size = U_BRK_SAFECLONE_BUFFERSIZE;
-
-	return ubrk_safeClone(global_break_iterator, stack_buffer, &buffer_size, status);
-#endif
+	return intl_icu_compat_ubrk_clone(global_break_iterator, status);
 }
 /* }}} */
diff --git a/ext/intl/grapheme/grapheme_util.h b/ext/intl/grapheme/grapheme_util.h
index a1d67981d27..0ddfcd3da88 100644
--- a/ext/intl/grapheme/grapheme_util.h
+++ b/ext/intl/grapheme/grapheme_util.h
@@ -22,7 +22,7 @@ extern "C" {
 #include "intl_convert.h"

 /* get_break_interator: get a break iterator from the global structure */
-UBreakIterator* grapheme_get_break_iterator(void *stack_buffer, UErrorCode *status );
+UBreakIterator* grapheme_get_break_iterator(UErrorCode *status );

 zend_long grapheme_ascii_check(const unsigned char *day, size_t len);
 void grapheme_substr_ascii(char *str, size_t str_len, int32_t f, int32_t l, char **sub_str, int32_t *sub_str_len);
@@ -37,7 +37,7 @@ int32_t grapheme_count_graphemes(UBreakIterator *bi, UChar *string, int32_t stri

 int32_t grapheme_get_haystack_offset(UBreakIterator* bi, int32_t offset);

-UBreakIterator* grapheme_get_break_iterator(void *stack_buffer, UErrorCode *status );
+UBreakIterator* grapheme_get_break_iterator(UErrorCode *status );
 #ifdef __cplusplus
 }
 #endif
diff --git a/ext/intl/intl_icu_compat.c b/ext/intl/intl_icu_compat.c
new file mode 100644
index 00000000000..5aa12851cfb
--- /dev/null
+++ b/ext/intl/intl_icu_compat.c
@@ -0,0 +1,114 @@
+/*
+   +----------------------------------------------------------------------+
+   | Copyright (c) The PHP Group                                          |
+   +----------------------------------------------------------------------+
+   | 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 <https://www.php.net/license/>.        |
+   |                                                                      |
+   | SPDX-License-Identifier: BSD-3-Clause                                |
+   +----------------------------------------------------------------------+
+   | Authors: Weilin Du <weilindu@php.net>                                |
+   +----------------------------------------------------------------------+
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stddef.h>
+
+#include "intl_icu_compat.h"
+#include <unicode/ubrk.h>
+#include <unicode/ucnv.h>
+#include <unicode/uspoof.h>
+#include <unicode/uset.h>
+
+UConverter *intl_icu_compat_ucnv_clone(const UConverter *converter, UErrorCode *status)
+{
+#if INTL_ICU_HAS_UCNV_CLONE
+	return ucnv_clone(converter, status);
+#else
+	return ucnv_safeClone(converter, NULL, NULL, status);
+#endif
+}
+
+UBreakIterator *intl_icu_compat_ubrk_clone(const UBreakIterator *break_iterator, UErrorCode *status)
+{
+#if INTL_ICU_HAS_UBRK_CLONE
+	return ubrk_clone(break_iterator, status);
+#else
+	return ubrk_safeClone(break_iterator, NULL, NULL, status);
+#endif
+}
+
+void intl_icu_compat_uspoof_init_checker(USpoofChecker *checker, IntlIcuSpoofCheckResult **check_result, UErrorCode *status)
+{
+#if INTL_ICU_HAS_SPOOFCHECKER_CHECK_RESULT
+	/* ICU 58 removed WSC/MSC handling in favor of TR39 restriction levels.
+	 * Keep the default highly restrictive behavior and allocate the extended
+	 * check result used by uspoof_check2*(). */
+	uspoof_setRestrictionLevel(checker, USPOOF_HIGHLY_RESTRICTIVE);
+	*check_result = uspoof_openCheckResult(status);
+#else
+	int checks = uspoof_getChecks(checker, status);
+
+	/* Single-script enforcement rejects legitimate mixed-script languages like Japanese. */
+	uspoof_setChecks(checker, checks & ~USPOOF_SINGLE_SCRIPT, status);
+	*check_result = NULL;
+#endif
+}
+
+void intl_icu_compat_uspoof_close_check_result(IntlIcuSpoofCheckResult *check_result)
+{
+#if INTL_ICU_HAS_SPOOFCHECKER_CHECK_RESULT
+	if (check_result) {
+		uspoof_closeCheckResult(check_result);
+	}
+#else
+	(void) check_result;
+#endif
+}
+
+int32_t intl_icu_compat_uspoof_check_utf8(const USpoofChecker *checker, const char *text, int32_t length, IntlIcuSpoofCheckResult *check_result, UErrorCode *status)
+{
+#if INTL_ICU_HAS_SPOOFCHECKER_CHECK_RESULT
+	return uspoof_check2UTF8(checker, text, length, check_result, status);
+#else
+	(void) check_result;
+	return uspoof_checkUTF8(checker, text, length, NULL, status);
+#endif
+}
+
+UBool intl_icu_compat_uspoof_check_result_mismatch(IntlIcuSpoofCheckResult *check_result, int32_t checks, int32_t *result_checks, UErrorCode *status)
+{
+#if INTL_ICU_HAS_SPOOFCHECKER_CHECK_RESULT
+	*result_checks = uspoof_getCheckResultChecks(check_result, status);
+	return *result_checks != checks;
+#else
+	(void) check_result;
+	(void) status;
+	*result_checks = checks;
+	return 0;
+#endif
+}
+
+UBool intl_icu_compat_uspoof_is_allowed_chars_pattern_option(int64_t pattern_option)
+{
+	return pattern_option == 0
+		|| pattern_option == USET_IGNORE_SPACE
+#if INTL_ICU_HAS_USET_SIMPLE_CASE_INSENSITIVE
+		|| pattern_option == (USET_IGNORE_SPACE|USET_SIMPLE_CASE_INSENSITIVE)
+#endif
+		|| pattern_option == (USET_IGNORE_SPACE|USET_CASE_INSENSITIVE)
+		|| pattern_option == (USET_IGNORE_SPACE|USET_ADD_CASE_MAPPINGS);
+}
+
+const char *intl_icu_compat_uspoof_allowed_chars_pattern_option_error_message(void)
+{
+#if INTL_ICU_HAS_USET_SIMPLE_CASE_INSENSITIVE
+	return "must be a valid pattern option, 0 or (SpoofChecker::IGNORE_SPACE|(<none> or SpoofChecker::CASE_INSENSITIVE or SpoofChecker::ADD_CASE_MAPPINGS or SpoofChecker::SIMPLE_CASE_INSENSITIVE))";
+#else
+	return "must be a valid pattern option, 0 or (SpoofChecker::IGNORE_SPACE|(<none> or SpoofChecker::CASE_INSENSITIVE or SpoofChecker::ADD_CASE_MAPPINGS))";
+#endif
+}
diff --git a/ext/intl/intl_icu_compat.h b/ext/intl/intl_icu_compat.h
new file mode 100644
index 00000000000..ae3a6aa126e
--- /dev/null
+++ b/ext/intl/intl_icu_compat.h
@@ -0,0 +1,56 @@
+/*
+   +----------------------------------------------------------------------+
+   | Copyright (c) The PHP Group                                          |
+   +----------------------------------------------------------------------+
+   | 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 <https://www.php.net/license/>.        |
+   |                                                                      |
+   | SPDX-License-Identifier: BSD-3-Clause                                |
+   +----------------------------------------------------------------------+
+   | Authors: Weilin Du <weilindu@php.net>                                |
+   +----------------------------------------------------------------------+
+ */
+
+#ifndef INTL_ICU_COMPAT_H
+#define INTL_ICU_COMPAT_H
+
+#include <unicode/ubrk.h>
+#include <unicode/ucnv.h>
+#include <unicode/uspoof.h>
+#include <unicode/utypes.h>
+#include <unicode/uversion.h>
+
+#define INTL_ICU_VERSION_AT_LEAST(major, minor) \
+	(U_ICU_VERSION_MAJOR_NUM > (major) || \
+		(U_ICU_VERSION_MAJOR_NUM == (major) && U_ICU_VERSION_MINOR_NUM >= (minor)))
+
+#define INTL_ICU_HAS_UBRK_CLONE INTL_ICU_VERSION_AT_LEAST(69, 0)
+#define INTL_ICU_HAS_UCNV_CLONE INTL_ICU_VERSION_AT_LEAST(71, 0)
+#define INTL_ICU_HAS_SPOOFCHECKER_CHECK_RESULT INTL_ICU_VERSION_AT_LEAST(58, 0)
+#define INTL_ICU_HAS_USET_SIMPLE_CASE_INSENSITIVE INTL_ICU_VERSION_AT_LEAST(73, 0)
+
+#if INTL_ICU_HAS_SPOOFCHECKER_CHECK_RESULT
+typedef USpoofCheckResult IntlIcuSpoofCheckResult;
+#else
+typedef void IntlIcuSpoofCheckResult;
+#endif
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+UConverter *intl_icu_compat_ucnv_clone(const UConverter *converter, UErrorCode *status);
+UBreakIterator *intl_icu_compat_ubrk_clone(const UBreakIterator *break_iterator, UErrorCode *status);
+void intl_icu_compat_uspoof_init_checker(USpoofChecker *checker, IntlIcuSpoofCheckResult **check_result, UErrorCode *status);
+void intl_icu_compat_uspoof_close_check_result(IntlIcuSpoofCheckResult *check_result);
+int32_t intl_icu_compat_uspoof_check_utf8(const USpoofChecker *checker, const char *text, int32_t length, IntlIcuSpoofCheckResult *check_result, UErrorCode *status);
+UBool intl_icu_compat_uspoof_check_result_mismatch(IntlIcuSpoofCheckResult *check_result, int32_t checks, int32_t *result_checks, UErrorCode *status);
+UBool intl_icu_compat_uspoof_is_allowed_chars_pattern_option(int64_t pattern_option);
+const char *intl_icu_compat_uspoof_allowed_chars_pattern_option_error_message(void);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/ext/intl/spoofchecker/spoofchecker_class.cpp b/ext/intl/spoofchecker/spoofchecker_class.cpp
index 6f072081fa7..7f5f66bcc80 100644
--- a/ext/intl/spoofchecker/spoofchecker_class.cpp
+++ b/ext/intl/spoofchecker/spoofchecker_class.cpp
@@ -132,12 +132,8 @@ U_CFUNC void spoofchecker_object_destroy(Spoofchecker_object* co)
 		co->uspoof = NULL;
 	}

-#if U_ICU_VERSION_MAJOR_NUM >= 58
-	if (co->uspoofres) {
-		uspoof_closeCheckResult(co->uspoofres);
-		co->uspoofres = NULL;
-	}
-#endif
+	intl_icu_compat_uspoof_close_check_result(co->uspoofres);
+	co->uspoofres = NULL;

 	intl_error_reset(SPOOFCHECKER_ERROR_P(co));
 }
diff --git a/ext/intl/spoofchecker/spoofchecker_class.h b/ext/intl/spoofchecker/spoofchecker_class.h
index 8871ca2a5b2..68d5daf2b5a 100644
--- a/ext/intl/spoofchecker/spoofchecker_class.h
+++ b/ext/intl/spoofchecker/spoofchecker_class.h
@@ -27,6 +27,7 @@ extern "C" {
 }
 #endif

+#include "intl_icu_compat.h"
 #include <unicode/uspoof.h>

 typedef struct {
@@ -35,9 +36,7 @@ typedef struct {

 	// ICU Spoofchecker
 	USpoofChecker*     uspoof;
-#if U_ICU_VERSION_MAJOR_NUM >= 58
-	USpoofCheckResult* uspoofres;
-#endif
+	IntlIcuSpoofCheckResult* uspoofres;

 	zend_object     zo;
 } Spoofchecker_object;
@@ -87,8 +86,4 @@ extern zend_class_entry *Spoofchecker_ce_ptr;
         RETURN_FALSE;                                                           \
     }                                                                           \

-#if U_ICU_VERSION_MAJOR_NUM >= 58
-#define SPOOFCHECKER_DEFAULT_RESTRICTION_LEVEL USPOOF_HIGHLY_RESTRICTIVE
-#endif
-
 #endif // #ifndef SPOOFCHECKER_CLASS_H
diff --git a/ext/intl/spoofchecker/spoofchecker_create.cpp b/ext/intl/spoofchecker/spoofchecker_create.cpp
index df032f91035..8a322c0d64c 100644
--- a/ext/intl/spoofchecker/spoofchecker_create.cpp
+++ b/ext/intl/spoofchecker/spoofchecker_create.cpp
@@ -30,9 +30,6 @@ extern "C" {
 /* {{{ Spoofchecker object constructor. */
 U_CFUNC PHP_METHOD(Spoofchecker, __construct)
 {
-#if U_ICU_VERSION_MAJOR_NUM < 58
-	int checks;
-#endif
 	SPOOFCHECKER_METHOD_INIT_VARS;

 	ZEND_PARSE_PARAMETERS_NONE();
@@ -49,23 +46,7 @@ U_CFUNC PHP_METHOD(Spoofchecker, __construct)
 			"Spoofchecker::__construct(): unable to open ICU Spoof Checker", 0);
 	}

-#if U_ICU_VERSION_MAJOR_NUM >= 58
-	/* TODO save it into the object for further suspiction check comparison. */
-	/* ICU 58 removes WSC and MSC handling. However there are restriction
-	 levels as defined in
-	 http://www.unicode.org/reports/tr39/tr39-15.html#Restriction_Level_Detection
-	 and the default is high restrictive. In further, we might want to utilize
-	 uspoof_check2 APIs when it became stable, to use extended check result APIs.
-	 Subsequent changes in the unicode security algos are to be watched.*/
-	uspoof_setRestrictionLevel(co->uspoof, SPOOFCHECKER_DEFAULT_RESTRICTION_LEVEL);
-	co->uspoofres = uspoof_openCheckResult(SPOOFCHECKER_ERROR_CODE_P(co));
-#else
-	/* Single-script enforcement is on by default. This fails for languages
-	 like Japanese that legally use multiple scripts within a single word,
-	 so we turn it off.
-	*/
-	checks = uspoof_getChecks(co->uspoof, SPOOFCHECKER_ERROR_CODE_P(co));
-	uspoof_setChecks(co->uspoof, checks & ~USPOOF_SINGLE_SCRIPT, SPOOFCHECKER_ERROR_CODE_P(co));
-#endif
+	/* Applies the default spoof checker behavior consistently across ICU versions. */
+	intl_icu_compat_uspoof_init_checker(co->uspoof, &co->uspoofres, SPOOFCHECKER_ERROR_CODE_P(co));
 }
 /* }}} */
diff --git a/ext/intl/spoofchecker/spoofchecker_main.cpp b/ext/intl/spoofchecker/spoofchecker_main.cpp
index 73e14cabeaf..cfd5ad8fb63 100644
--- a/ext/intl/spoofchecker/spoofchecker_main.cpp
+++ b/ext/intl/spoofchecker/spoofchecker_main.cpp
@@ -43,21 +43,14 @@ U_CFUNC PHP_METHOD(Spoofchecker, isSuspicious)

 	SPOOFCHECKER_METHOD_FETCH_OBJECT;

-#if U_ICU_VERSION_MAJOR_NUM >= 58
-	ret = uspoof_check2UTF8(co->uspoof, ZSTR_VAL(text), ZSTR_LEN(text), co->uspoofres, SPOOFCHECKER_ERROR_CODE_P(co));
-#else
-	ret = uspoof_checkUTF8(co->uspoof, ZSTR_VAL(text), ZSTR_LEN(text), NULL, SPOOFCHECKER_ERROR_CODE_P(co));
-#endif
+	ret = intl_icu_compat_uspoof_check_utf8(co->uspoof, ZSTR_VAL(text), ZSTR_LEN(text), co->uspoofres, SPOOFCHECKER_ERROR_CODE_P(co));

 	if (U_FAILURE(SPOOFCHECKER_ERROR_CODE(co))) {
 		php_error_docref(NULL, E_WARNING, "(%d) %s", SPOOFCHECKER_ERROR_CODE(co), u_errorName(SPOOFCHECKER_ERROR_CODE(co)));
-#if U_ICU_VERSION_MAJOR_NUM >= 58
-		errmask = uspoof_getCheckResultChecks(co->uspoofres, SPOOFCHECKER_ERROR_CODE_P(co));

-		if (errmask != ret) {
+		if (intl_icu_compat_uspoof_check_result_mismatch(co->uspoofres, ret, &errmask, SPOOFCHECKER_ERROR_CODE_P(co))) {
 			php_error_docref(NULL, E_WARNING, "unexpected error (%d), does not relate to the flags passed to setChecks (%d)", ret, errmask);
 		}
-#endif
 		RETURN_TRUE;
 	}

@@ -206,19 +199,8 @@ U_CFUNC PHP_METHOD(Spoofchecker, setAllowedChars)
 	USet *set = uset_openEmpty();

 	/* pattern is either USE_IGNORE_SPACE alone or in conjunction with the following flags (but mutually exclusive) */
-	if (pattern_option &&
-            pattern_option != USET_IGNORE_SPACE &&
-#if U_ICU_VERSION_MAJOR_NUM >= 73
-            pattern_option != (USET_IGNORE_SPACE|USET_SIMPLE_CASE_INSENSITIVE) &&
-#endif
-            pattern_option != (USET_IGNORE_SPACE|USET_CASE_INSENSITIVE) &&
-            pattern_option != (USET_IGNORE_SPACE|USET_ADD_CASE_MAPPINGS)) {
-		zend_argument_value_error(2, "must be a valid pattern option, 0 or (SpoofChecker::IGNORE_SPACE|(<none> or SpoofChecker::CASE_INSENSITIVE or SpoofChecker::ADD_CASE_MAPPINGS"
-#if U_ICU_VERSION_MAJOR_NUM >= 73
-				" or SpoofChecker::SIMPLE_CASE_INSENSITIVE"
-#endif
-				"))"
-		);
+	if (!intl_icu_compat_uspoof_is_allowed_chars_pattern_option(pattern_option)) {
+		zend_argument_value_error(2, "%s", intl_icu_compat_uspoof_allowed_chars_pattern_option_error_message());
 		uset_close(set);
 		efree(upattern);
 		RETURN_THROWS();