Commit f0e88c7ff3b for php

commit f0e88c7ff3bca9501374a9e49ab003746a8b5e45
Author: Weilin Du <weilindu@php.net>
Date:   Sun Oct 18 17:49:41 2026 +0800

    ext/intl: Add Transliterator::toRules() (#24384)

    Expose transliterator rules through Transliterator::toRules() and
    transliterator_to_rules(), with optional escaping of unprintable characters.
    Use ICU's C++ API to export the rules into a UnicodeString.

    Co-authored-by: David Carlier <devnexen@gmail.com>

diff --git a/NEWS b/NEWS
index 4a9a2135ee2..faee75f6942 100644
--- a/NEWS
+++ b/NEWS
@@ -18,6 +18,8 @@ PHP                                                                        NEWS
     throwing a ValueError. (Marc Bennewitz)

 - Intl:
+  . Added Transliterator::toRules() and transliterator_to_rules().
+    (Weilin Du, David Carlier)
   . Fixed Collator attribute and strength methods not rejecting an
     unconstructed Collator. (Ilia Alshanetsky)
   . Added optional $availableLocales argument to Locale::acceptFromHttp()
diff --git a/UPGRADING b/UPGRADING
index 9442aad8450..0c3616dec07 100644
--- a/UPGRADING
+++ b/UPGRADING
@@ -85,6 +85,11 @@ PHP 8.7 UPGRADE NOTES
 6. New Functions
 ========================================

+- Intl:
+  . Added Transliterator::toRules() and transliterator_to_rules() to export
+    rules that can be used to recreate a transliterator. The optional
+    $escapeUnprintable argument escapes unprintable characters when true.
+
 ========================================
 7. New Classes and Interfaces
 ========================================
diff --git a/ext/intl/php_intl.stub.php b/ext/intl/php_intl.stub.php
index a30c2d98760..a6a519e50f6 100644
--- a/ext/intl/php_intl.stub.php
+++ b/ext/intl/php_intl.stub.php
@@ -640,6 +640,8 @@ function transliterator_list_ids(): array|false {}

 function transliterator_create_inverse(Transliterator $transliterator): ?Transliterator {}

+function transliterator_to_rules(Transliterator $transliterator, bool $escapeUnprintable = false): string|false {}
+
 function transliterator_transliterate(Transliterator|string $transliterator, string $string, int $start = 0, int $end = -1): string|false {}

 function transliterator_get_error_code(Transliterator $transliterator): int {}
diff --git a/ext/intl/php_intl_arginfo.h b/ext/intl/php_intl_arginfo.h
index 9478a818d38..a1b95671d27 100644
Binary files a/ext/intl/php_intl_arginfo.h and b/ext/intl/php_intl_arginfo.h differ
diff --git a/ext/intl/tests/transliterator_to_rules_basic.phpt b/ext/intl/tests/transliterator_to_rules_basic.phpt
new file mode 100644
index 00000000000..0a3be7fac96
--- /dev/null
+++ b/ext/intl/tests/transliterator_to_rules_basic.phpt
@@ -0,0 +1,27 @@
+--TEST--
+Transliterator::toRules() and transliterator_to_rules()
+--EXTENSIONS--
+intl
+--FILE--
+<?php
+$transliterator = Transliterator::createFromRules('a > b;');
+var_dump($transliterator->toRules());
+var_dump(transliterator_to_rules($transliterator));
+
+foreach ([
+    Transliterator::create('Any-Lower; Latin-ASCII'),
+    Transliterator::createFromRules('a <> b;', Transliterator::REVERSE),
+    Transliterator::createFromRules(''),
+    Transliterator::createFromRules('a > ' . str_repeat('b', 4096) . ';'),
+] as $transliterator) {
+    $copy = Transliterator::createFromRules($transliterator->toRules());
+    var_dump($copy->transliterate('ab ÄÉ') === $transliterator->transliterate('ab ÄÉ'));
+}
+?>
+--EXPECT--
+string(6) "a > b;"
+string(6) "a > b;"
+bool(true)
+bool(true)
+bool(true)
+bool(true)
diff --git a/ext/intl/tests/transliterator_to_rules_error.phpt b/ext/intl/tests/transliterator_to_rules_error.phpt
new file mode 100644
index 00000000000..e1381bcd723
--- /dev/null
+++ b/ext/intl/tests/transliterator_to_rules_error.phpt
@@ -0,0 +1,27 @@
+--TEST--
+Transliterator::toRules() validates objects and resets previous errors
+--EXTENSIONS--
+intl
+--INI--
+intl.error_level=0
+--FILE--
+<?php
+$transliterator = Transliterator::createFromRules('a > b;');
+var_dump($transliterator->transliterate("\xff"));
+var_dump($transliterator->toRules());
+var_dump($transliterator->getErrorCode(), intl_get_error_code());
+
+class UnconstructedTransliterator extends Transliterator {}
+$unconstructed = (new ReflectionClass(UnconstructedTransliterator::class))->newInstanceWithoutConstructor();
+try {
+    $unconstructed->toRules();
+} catch (Error $e) {
+    echo $e->getMessage(), "\n";
+}
+?>
+--EXPECT--
+bool(false)
+string(6) "a > b;"
+int(0)
+int(0)
+Found unconstructed transliterator
diff --git a/ext/intl/tests/transliterator_to_rules_unicode.phpt b/ext/intl/tests/transliterator_to_rules_unicode.phpt
new file mode 100644
index 00000000000..77d2e213aff
--- /dev/null
+++ b/ext/intl/tests/transliterator_to_rules_unicode.phpt
@@ -0,0 +1,33 @@
+--TEST--
+Transliterator::toRules() escapes Unicode characters and preserves rules
+--EXTENSIONS--
+intl
+--FILE--
+<?php
+$transliterator = Transliterator::createFromRules('α > a; \U0001F600 > x;');
+$rules = $transliterator->toRules();
+var_dump(str_contains($rules, 'α'));
+var_dump(str_contains($rules, '😀'));
+
+$escaped = transliterator_to_rules($transliterator, escapeUnprintable: true);
+var_dump(preg_match('/[^\x00-\x7f]/', $escaped));
+
+foreach ([$rules, $escaped] as $exported) {
+    $copy = Transliterator::createFromRules($exported);
+    var_dump($copy->transliterate('α😀'));
+}
+
+// Preserve embedded NULs in the output; escape them to make the rules parseable.
+$transliterator = Transliterator::createFromRules('\u0000 > z;');
+var_dump(str_contains($transliterator->toRules(), "\0"));
+$escaped = $transliterator->toRules(true);
+var_dump(Transliterator::createFromRules($escaped)->transliterate("\0"));
+?>
+--EXPECT--
+bool(true)
+bool(true)
+int(0)
+string(2) "ax"
+string(2) "ax"
+bool(true)
+string(1) "z"
diff --git a/ext/intl/transliterator/transliterator.stub.php b/ext/intl/transliterator/transliterator.stub.php
index df65b8c7f0d..198c6fea0c3 100644
--- a/ext/intl/transliterator/transliterator.stub.php
+++ b/ext/intl/transliterator/transliterator.stub.php
@@ -32,6 +32,9 @@ public static function createFromRules(string $rules, int $direction = Translite
      */
     public function createInverse(): ?Transliterator {}

+    /** @alias transliterator_to_rules */
+    public function toRules(bool $escapeUnprintable = false): string|false {}
+
     /**
      * @return array<int, string>|false
      * @tentative-return-type
diff --git a/ext/intl/transliterator/transliterator_arginfo.h b/ext/intl/transliterator/transliterator_arginfo.h
index 2d9e80708d1..42257294c3a 100644
Binary files a/ext/intl/transliterator/transliterator_arginfo.h and b/ext/intl/transliterator/transliterator_arginfo.h differ
diff --git a/ext/intl/transliterator/transliterator_methods.cpp b/ext/intl/transliterator/transliterator_methods.cpp
index d84fdbbe36a..db92be76824 100644
--- a/ext/intl/transliterator/transliterator_methods.cpp
+++ b/ext/intl/transliterator/transliterator_methods.cpp
@@ -22,6 +22,8 @@
 #endif

 #include <unicode/uenum.h>
+#include <unicode/translit.h>
+#include "../intl_convertcpp.h"

 extern "C" {
 #include "php_intl.h"
@@ -222,6 +224,33 @@ U_CFUNC PHP_FUNCTION( transliterator_create_inverse )
 }
 /* }}} */

+/* {{{ Export the rules used to recreate a transliterator. */
+U_CFUNC PHP_FUNCTION( transliterator_to_rules )
+{
+	bool escape_unprintable = false;
+	TRANSLITERATOR_METHOD_INIT_VARS;
+
+	if (zend_parse_method_parameters(ZEND_NUM_ARGS(), getThis(), "O|b",
+			&object, Transliterator_ce_ptr, &escape_unprintable) == FAILURE) {
+		RETURN_THROWS();
+	}
+
+	TRANSLITERATOR_METHOD_FETCH_OBJECT;
+
+	UnicodeString rules;
+	reinterpret_cast<const icu::Transliterator *>(to->utrans)->toRules(rules, escape_unprintable);
+
+	zend_string *u8str = intl_charFromString(rules, TRANSLITERATOR_ERROR_CODE_P(to));
+	if (!u8str) {
+		if (U_SUCCESS(TRANSLITERATOR_ERROR_CODE(to))) {
+			TRANSLITERATOR_ERROR_CODE(to) = U_MEMORY_ALLOCATION_ERROR;
+		}
+		INTL_METHOD_CHECK_STATUS(to, "Failed to export transliterator rules");
+	}
+	RETURN_STR(u8str);
+}
+/* }}} */
+
 /* {{{ Return an array with the registered transliterator IDs. */
 PHP_INTL_FUNCTION_WITH_ERROR_RESET(transliterator_list_ids)
 {