Commit 3f285f41fca for php.net

commit 3f285f41fcaa4eee6ecd1afccccf555232b3a906
Author: Máté Kocsis <kocsismate@woohoolabs.com>
Date:   Wed Jun 24 11:04:22 2026 +0200

    Implement "Followup improvements for ext/uri" RFC - RFC 3986 URI building (#22173)

    RFC: https://wiki.php.net/rfc/uri_followup#uri_building

diff --git a/NEWS b/NEWS
index 3e24c2fa0bc..231e1788bbe 100644
--- a/NEWS
+++ b/NEWS
@@ -299,6 +299,8 @@ PHP                                                                        NEWS
     (kocsismate)
   . Added Uri\Rfc3986\Uri:getHostType() and Uri\WhatWg\Url:getHostType().
     (kocsismate)
+  . Added Uri\Rfc3986\UriBuilder.
+    (kocsismate)

 - Zip:
   . Fixed ZipArchive callback being called after executor has shut down.
diff --git a/UPGRADING b/UPGRADING
index 18afee1a5c9..14bbb9ecc39 100644
--- a/UPGRADING
+++ b/UPGRADING
@@ -254,6 +254,8 @@ PHP 8.6 UPGRADE NOTES
     RFC: https://wiki.php.net/rfc/uri_followup#uri_type_detection
   . Added Uri\Rfc3986\Uri:getHostType() and Uri\WhatWg\Url:getHostType().
     RFC: https://wiki.php.net/rfc/uri_followup#host_type_detection
+  . Added Uri\Rfc3986\UriBuilder.
+    RFC: https://wiki.php.net/rfc/uri_followup#uri_building

 ========================================
 3. Changes in SAPI modules
diff --git a/ext/uri/php_uri.c b/ext/uri/php_uri.c
index 58f34a37015..74a559fd591 100644
--- a/ext/uri/php_uri.c
+++ b/ext/uri/php_uri.c
@@ -30,6 +30,7 @@
 #include "php_uri_arginfo.h"
 #include "uriparser/Uri.h"

+zend_class_entry *php_uri_ce_rfc3986_uri_builder;
 zend_class_entry *php_uri_ce_rfc3986_uri;
 zend_class_entry *php_uri_ce_rfc3986_uri_type;
 zend_class_entry *php_uri_ce_rfc3986_uri_host_type;
@@ -46,6 +47,9 @@ zend_class_entry *php_uri_ce_whatwg_url_validation_error;
 static zend_object_handlers object_handlers_rfc3986_uri;
 static zend_object_handlers object_handlers_whatwg_uri;

+typedef zend_result (*php_uri_component_validator_string)(const zend_string *component);
+typedef zend_result (*php_uri_component_validator_long)(zend_long component);
+
 static const zend_module_dep uri_deps[] = {
 	ZEND_MOD_REQUIRED("lexbor")
 	ZEND_MOD_END
@@ -53,6 +57,23 @@ static const zend_module_dep uri_deps[] = {

 static zend_array uri_parsers;

+static zend_always_inline zval *php_uri_deref(zval *zv)
+{
+	if (UNEXPECTED(Z_TYPE_P(zv) == IS_REFERENCE)) {
+		return Z_REFVAL_P(zv);
+	}
+
+	return zv;
+}
+
+#define Z_RFC3986_URI_PROP_SCHEME_P(zv) php_uri_deref(OBJ_PROP_NUM(Z_OBJ_P(zv), 0))
+#define Z_RFC3986_URI_PROP_USERINFO_P(zv) php_uri_deref(OBJ_PROP_NUM(Z_OBJ_P(zv), 1))
+#define Z_RFC3986_URI_PROP_HOST_P(zv) php_uri_deref(OBJ_PROP_NUM(Z_OBJ_P(zv), 2))
+#define Z_RFC3986_URI_PROP_PORT_P(zv) php_uri_deref(OBJ_PROP_NUM(Z_OBJ_P(zv), 3))
+#define Z_RFC3986_URI_PROP_PATH_P(zv) php_uri_deref(OBJ_PROP_NUM(Z_OBJ_P(zv), 4))
+#define Z_RFC3986_URI_PROP_QUERY_P(zv) php_uri_deref(OBJ_PROP_NUM(Z_OBJ_P(zv), 5))
+#define Z_RFC3986_URI_PROP_FRAGMENT_P(zv) php_uri_deref(OBJ_PROP_NUM(Z_OBJ_P(zv), 6))
+
 static HashTable *uri_get_debug_properties(php_uri_object *object)
 {
 	const HashTable *std_properties = zend_std_get_properties(&object->std);
@@ -1044,6 +1065,186 @@ PHP_METHOD(Uri_WhatWg_Url, __debugInfo)
 	RETURN_ARR(uri_get_debug_properties(uri_object));
 }

+PHP_METHOD(Uri_Rfc3986_UriBuilder, reset)
+{
+	ZEND_PARSE_PARAMETERS_NONE();
+
+	convert_to_null(Z_RFC3986_URI_PROP_SCHEME_P(ZEND_THIS));
+	convert_to_null(Z_RFC3986_URI_PROP_USERINFO_P(ZEND_THIS));
+	convert_to_null(Z_RFC3986_URI_PROP_HOST_P(ZEND_THIS));
+	convert_to_null(Z_RFC3986_URI_PROP_PORT_P(ZEND_THIS));
+	zval_ptr_dtor(Z_RFC3986_URI_PROP_PATH_P(ZEND_THIS));
+	ZVAL_EMPTY_STRING(Z_RFC3986_URI_PROP_PATH_P(ZEND_THIS));
+	convert_to_null(Z_RFC3986_URI_PROP_QUERY_P(ZEND_THIS));
+	convert_to_null(Z_RFC3986_URI_PROP_FRAGMENT_P(ZEND_THIS));
+
+	RETVAL_COPY(ZEND_THIS);
+}
+
+ZEND_ATTRIBUTE_NONNULL static void php_uri_builder_set_component_string(
+	INTERNAL_FUNCTION_PARAMETERS, const char *name, const size_t name_length,
+	const php_uri_component_validator_string validator
+) {
+	zend_string *component;
+
+	ZEND_PARSE_PARAMETERS_START(1, 1)
+		Z_PARAM_STR(component)
+	ZEND_PARSE_PARAMETERS_END();
+
+	if (validator(component) == FAILURE) {
+		RETURN_THROWS();
+	}
+
+	zend_update_property_str(Z_OBJCE_P(ZEND_THIS), Z_OBJ_P(ZEND_THIS), name, name_length, component);
+
+	RETVAL_COPY(ZEND_THIS);
+}
+
+ZEND_ATTRIBUTE_NONNULL static void php_uri_builder_set_component_string_or_null(
+	INTERNAL_FUNCTION_PARAMETERS, const char *name, const size_t name_length,
+	const php_uri_component_validator_string validator
+) {
+	zend_string *component;
+
+	ZEND_PARSE_PARAMETERS_START(1, 1)
+		Z_PARAM_STR_OR_NULL(component)
+	ZEND_PARSE_PARAMETERS_END();
+
+	if (component == NULL) {
+		zend_update_property_null(Z_OBJCE_P(ZEND_THIS), Z_OBJ_P(ZEND_THIS), name, name_length);
+	} else {
+		if (validator(component) == FAILURE) {
+			RETURN_THROWS();
+		}
+
+		zend_update_property_str(Z_OBJCE_P(ZEND_THIS), Z_OBJ_P(ZEND_THIS), name, name_length, component);
+	}
+
+	RETVAL_COPY(ZEND_THIS);
+}
+
+ZEND_ATTRIBUTE_NONNULL_ARGS(1) static void php_uri_builder_set_component_long_or_null(
+	INTERNAL_FUNCTION_PARAMETERS, const char *name, const size_t name_length,
+	const php_uri_component_validator_long validator
+) {
+	zend_long component;
+	bool component_is_null;
+
+	ZEND_PARSE_PARAMETERS_START(1, 1)
+		Z_PARAM_LONG_OR_NULL(component, component_is_null)
+	ZEND_PARSE_PARAMETERS_END();
+
+	if (component_is_null) {
+		zend_update_property_null(Z_OBJCE_P(ZEND_THIS), Z_OBJ_P(ZEND_THIS), name, name_length);
+	} else {
+		if (validator(component) == FAILURE) {
+			RETURN_THROWS();
+		}
+
+		zend_update_property_long(Z_OBJCE_P(ZEND_THIS), Z_OBJ_P(ZEND_THIS), name, name_length, component);
+	}
+
+	RETVAL_COPY(ZEND_THIS);
+}
+
+PHP_METHOD(Uri_Rfc3986_UriBuilder, setScheme)
+{
+	php_uri_builder_set_component_string_or_null(
+		INTERNAL_FUNCTION_PARAM_PASSTHRU,
+		ZEND_STRL("scheme"),
+		php_uri_parser_rfc3986_validate_scheme
+	);
+}
+
+PHP_METHOD(Uri_Rfc3986_UriBuilder, setUserInfo)
+{
+	php_uri_builder_set_component_string_or_null(
+		INTERNAL_FUNCTION_PARAM_PASSTHRU,
+		ZEND_STRL("userinfo"),
+		php_uri_parser_rfc3986_validate_userinfo
+	);
+}
+
+PHP_METHOD(Uri_Rfc3986_UriBuilder, setHost)
+{
+	php_uri_builder_set_component_string_or_null(
+		INTERNAL_FUNCTION_PARAM_PASSTHRU,
+		ZEND_STRL("host"),
+		php_uri_parser_rfc3986_validate_host
+	);
+}
+
+PHP_METHOD(Uri_Rfc3986_UriBuilder, setPort)
+{
+	php_uri_builder_set_component_long_or_null(
+		INTERNAL_FUNCTION_PARAM_PASSTHRU,
+		ZEND_STRL("port"),
+		php_uri_parser_rfc3986_validate_port
+	);
+}
+
+PHP_METHOD(Uri_Rfc3986_UriBuilder, setPath)
+{
+	php_uri_builder_set_component_string(
+		INTERNAL_FUNCTION_PARAM_PASSTHRU,
+		ZEND_STRL("path"),
+		php_uri_parser_rfc3986_validate_path
+	);
+}
+
+PHP_METHOD(Uri_Rfc3986_UriBuilder, setQuery)
+{
+	php_uri_builder_set_component_string_or_null(
+		INTERNAL_FUNCTION_PARAM_PASSTHRU,
+		ZEND_STRL("query"),
+		php_uri_parser_rfc3986_validate_query
+	);
+}
+
+PHP_METHOD(Uri_Rfc3986_UriBuilder, setFragment)
+{
+	php_uri_builder_set_component_string_or_null(
+		INTERNAL_FUNCTION_PARAM_PASSTHRU,
+		ZEND_STRL("fragment"),
+		php_uri_parser_rfc3986_validate_fragment
+	);
+}
+
+PHP_METHOD(Uri_Rfc3986_UriBuilder, build)
+{
+	zval *base_url = NULL;
+
+	ZEND_PARSE_PARAMETERS_START(0, 1)
+		Z_PARAM_OPTIONAL
+		Z_PARAM_OBJECT_OF_CLASS_OR_NULL(base_url, php_uri_ce_rfc3986_uri)
+	ZEND_PARSE_PARAMETERS_END();
+
+	const zval *scheme = Z_RFC3986_URI_PROP_SCHEME_P(ZEND_THIS);
+	const zval *userinfo = Z_RFC3986_URI_PROP_USERINFO_P(ZEND_THIS);
+	const zval *host = Z_RFC3986_URI_PROP_HOST_P(ZEND_THIS);
+	const zval *port = Z_RFC3986_URI_PROP_PORT_P(ZEND_THIS);
+	const zval *path = Z_RFC3986_URI_PROP_PATH_P(ZEND_THIS);
+	const zval *query = Z_RFC3986_URI_PROP_QUERY_P(ZEND_THIS);
+	const zval *fragment = Z_RFC3986_URI_PROP_FRAGMENT_P(ZEND_THIS);
+
+	php_uri_parser_rfc3986_uris *base_uris = NULL;
+	if (base_url != NULL) {
+		base_uris = Z_URI_OBJECT_P(base_url)->uri;
+	}
+
+	php_uri_parser_rfc3986_uris *uriparser_uris = php_uri_parser_rfc3986_build_from_zval(
+		base_uris, scheme, userinfo, host, port, path, query, fragment
+	);
+	if (uriparser_uris == NULL) {
+		RETURN_THROWS();
+	}
+
+	object_init_ex(return_value, php_uri_ce_rfc3986_uri);
+	php_uri_object *uri_object = Z_URI_OBJECT_P(return_value);
+	uri_object->parser = &php_uri_parser_rfc3986;
+	uri_object->uri = uriparser_uris;
+}
+
 PHPAPI php_uri_object *php_uri_object_create(zend_class_entry *class_type, const php_uri_parser *parser)
 {
 	php_uri_object *uri_object = zend_object_alloc(sizeof(*uri_object), class_type);
@@ -1113,6 +1314,8 @@ PHPAPI zend_result php_uri_parser_register(const php_uri_parser *uri_parser)

 static PHP_MINIT_FUNCTION(uri)
 {
+	php_uri_ce_rfc3986_uri_builder = register_class_Uri_Rfc3986_UriBuilder();
+
 	php_uri_ce_rfc3986_uri = register_class_Uri_Rfc3986_Uri();
 	php_uri_ce_rfc3986_uri->create_object = php_uri_object_create_rfc3986;
 	php_uri_ce_rfc3986_uri->default_object_handlers = &object_handlers_rfc3986_uri;
diff --git a/ext/uri/php_uri.stub.php b/ext/uri/php_uri.stub.php
index b0b83fcf83e..d00ef45cb86 100644
--- a/ext/uri/php_uri.stub.php
+++ b/ext/uri/php_uri.stub.php
@@ -45,6 +45,35 @@ enum UriHostType
         case RegisteredName;
     }

+    final class UriBuilder
+    {
+        private ?string $scheme = null;
+        private ?string $userinfo = null;
+        private ?string $host = null;
+        private ?int $port = null;
+        private string $path = "";
+        private ?string $query = null;
+        private ?string $fragment = null;
+
+        public function reset(): static {}
+
+        public function setScheme(?string $scheme): static {}
+
+        public function setUserInfo(#[\SensitiveParameter] ?string $userInfo): static {}
+
+        public function setHost(?string $host): static {}
+
+        public function setPort(?int $port): static {}
+
+        public function setPath(string $path): static {}
+
+        public function setQuery(?string $query): static {}
+
+        public function setFragment(?string $fragment): static {}
+
+        public function build(?\Uri\Rfc3986\Uri $baseUrl = null): \Uri\Rfc3986\Uri {}
+    }
+
     /** @strict-properties */
     final readonly class Uri
     {
diff --git a/ext/uri/php_uri_arginfo.h b/ext/uri/php_uri_arginfo.h
index 0fb464ee74a..e63e495526f 100644
Binary files a/ext/uri/php_uri_arginfo.h and b/ext/uri/php_uri_arginfo.h differ
diff --git a/ext/uri/php_uri_common.h b/ext/uri/php_uri_common.h
index b79d092ae72..9106f6acd15 100644
--- a/ext/uri/php_uri_common.h
+++ b/ext/uri/php_uri_common.h
@@ -17,6 +17,7 @@

 #include "php_uri_decl.h"

+extern zend_class_entry *php_uri_ce_rfc3986_uri_builder;
 extern zend_class_entry *php_uri_ce_rfc3986_uri;
 extern zend_class_entry *php_uri_ce_rfc3986_uri_type;
 extern zend_class_entry *php_uri_ce_rfc3986_uri_host_type;
diff --git a/ext/uri/php_uri_decl.h b/ext/uri/php_uri_decl.h
index d1fd58d04b2..784ac5b4c0e 100644
Binary files a/ext/uri/php_uri_decl.h and b/ext/uri/php_uri_decl.h differ
diff --git a/ext/uri/tests/rfc3986/builder/all_success_with_reset.phpt b/ext/uri/tests/rfc3986/builder/all_success_with_reset.phpt
new file mode 100644
index 00000000000..9c11fb2e668
--- /dev/null
+++ b/ext/uri/tests/rfc3986/builder/all_success_with_reset.phpt
@@ -0,0 +1,67 @@
+--TEST--
+Test Uri\Rfc3986\UriBuilder all components - success - calling reset() afterwards
+--FILE--
+<?php
+
+$builder = new Uri\Rfc3986\UriBuilder()
+    ->setScheme("https")
+    ->setUserInfo("user:info")
+    ->setHost("example.com")
+    ->setPort(443)
+    ->setPath("/foo/bar/baz")
+    ->setQuery("foo=1&bar=baz")
+    ->setFragment("fragment");
+$uri = $builder->build();
+
+var_dump($uri->toRawString());
+var_dump($uri);
+var_dump($uri->equals(new Uri\Rfc3986\Uri($uri->toRawString())));
+
+$uri = $builder->reset()->build();
+
+var_dump($uri->toRawString());
+var_dump($uri);
+var_dump($uri->equals(new Uri\Rfc3986\Uri($uri->toRawString())));
+
+?>
+--EXPECTF--
+string(68) "https://user:info@example.com:443/foo/bar/baz?foo=1&bar=baz#fragment"
+object(Uri\Rfc3986\Uri)#%d (%d) {
+  ["scheme"]=>
+  string(5) "https"
+  ["username"]=>
+  string(4) "user"
+  ["password"]=>
+  string(4) "info"
+  ["host"]=>
+  string(11) "example.com"
+  ["port"]=>
+  int(443)
+  ["path"]=>
+  string(12) "/foo/bar/baz"
+  ["query"]=>
+  string(13) "foo=1&bar=baz"
+  ["fragment"]=>
+  string(8) "fragment"
+}
+bool(true)
+string(0) ""
+object(Uri\Rfc3986\Uri)#%d (%d) {
+  ["scheme"]=>
+  NULL
+  ["username"]=>
+  NULL
+  ["password"]=>
+  NULL
+  ["host"]=>
+  NULL
+  ["port"]=>
+  NULL
+  ["path"]=>
+  string(0) ""
+  ["query"]=>
+  NULL
+  ["fragment"]=>
+  NULL
+}
+bool(true)
diff --git a/ext/uri/tests/rfc3986/builder/basic_error_with_base.phpt b/ext/uri/tests/rfc3986/builder/basic_error_with_base.phpt
new file mode 100644
index 00000000000..284de4e6a52
--- /dev/null
+++ b/ext/uri/tests/rfc3986/builder/basic_error_with_base.phpt
@@ -0,0 +1,17 @@
+--TEST--
+Test Uri\Rfc3986\UriBuilder basic - error - with base URL
+--FILE--
+<?php
+
+$builder = new Uri\Rfc3986\UriBuilder()
+    ->setPath("/foo/bar/baz");
+
+try {
+    $builder->build(new Uri\Rfc3986\Uri("/foo/bar"));
+} catch (Throwable $e) {
+    echo $e::class, ": ", $e->getMessage(), PHP_EOL;
+}
+
+?>
+--EXPECT--
+Uri\InvalidUriException: The specified base URI must be absolute
diff --git a/ext/uri/tests/rfc3986/builder/basic_success_with_base.phpt b/ext/uri/tests/rfc3986/builder/basic_success_with_base.phpt
new file mode 100644
index 00000000000..b7a84dc00cf
--- /dev/null
+++ b/ext/uri/tests/rfc3986/builder/basic_success_with_base.phpt
@@ -0,0 +1,35 @@
+--TEST--
+Test Uri\Rfc3986\UriBuilder basic - success - with base URL
+--FILE--
+<?php
+
+$builder = new Uri\Rfc3986\UriBuilder()
+    ->setPath("/foo/bar/baz");
+$uri = $builder->build(new Uri\Rfc3986\Uri("https://example.com"));
+
+var_dump($uri->toRawString());
+var_dump($uri);
+var_dump($uri->equals(new Uri\Rfc3986\Uri($uri->toRawString())));
+
+?>
+--EXPECTF--
+string(31) "https://example.com/foo/bar/baz"
+object(Uri\Rfc3986\Uri)#%d (%d) {
+  ["scheme"]=>
+  string(5) "https"
+  ["username"]=>
+  NULL
+  ["password"]=>
+  NULL
+  ["host"]=>
+  string(11) "example.com"
+  ["port"]=>
+  NULL
+  ["path"]=>
+  string(12) "/foo/bar/baz"
+  ["query"]=>
+  NULL
+  ["fragment"]=>
+  NULL
+}
+bool(true)
diff --git a/ext/uri/tests/rfc3986/builder/fragment_error_special_char.phpt b/ext/uri/tests/rfc3986/builder/fragment_error_special_char.phpt
new file mode 100644
index 00000000000..8ed2a8938e9
--- /dev/null
+++ b/ext/uri/tests/rfc3986/builder/fragment_error_special_char.phpt
@@ -0,0 +1,16 @@
+--TEST--
+Test Uri\Rfc3986\UriBuilder::setFragment() - error - contains invalid special character
+--FILE--
+<?php
+
+$builder = new Uri\Rfc3986\UriBuilder();
+
+try {
+    $builder->setFragment("#foo");
+} catch (Throwable $e) {
+    echo $e::class, ": ", $e->getMessage(), PHP_EOL;
+}
+
+?>
+--EXPECT--
+Uri\InvalidUriException: The specified fragment is malformed
diff --git a/ext/uri/tests/rfc3986/builder/fragment_error_unicode_char.phpt b/ext/uri/tests/rfc3986/builder/fragment_error_unicode_char.phpt
new file mode 100644
index 00000000000..2a15d5e85a8
--- /dev/null
+++ b/ext/uri/tests/rfc3986/builder/fragment_error_unicode_char.phpt
@@ -0,0 +1,16 @@
+--TEST--
+Test Uri\Rfc3986\UriBuilder::setFragment() - error - contains Unicode character
+--FILE--
+<?php
+
+$builder = new Uri\Rfc3986\UriBuilder();
+
+try {
+    $builder->setFragment("főő");
+} catch (Throwable $e) {
+    echo $e::class, ": ", $e->getMessage(), PHP_EOL;
+}
+
+?>
+--EXPECT--
+Uri\InvalidUriException: The specified fragment is malformed
diff --git a/ext/uri/tests/rfc3986/builder/fragment_success_basic.phpt b/ext/uri/tests/rfc3986/builder/fragment_success_basic.phpt
new file mode 100644
index 00000000000..a59d9322e1b
--- /dev/null
+++ b/ext/uri/tests/rfc3986/builder/fragment_success_basic.phpt
@@ -0,0 +1,35 @@
+--TEST--
+Test Uri\Rfc3986\UriBuilder::setFragment() - success - basic
+--FILE--
+<?php
+
+$builder = new Uri\Rfc3986\UriBuilder();
+$builder->setFragment("foo");
+$uri = $builder->build();
+
+var_dump($uri->toRawString());
+var_dump($uri);
+var_dump($uri->equals(new Uri\Rfc3986\Uri($uri->toRawString())));
+
+?>
+--EXPECTF--
+string(4) "#foo"
+object(Uri\Rfc3986\Uri)#%d (%d) {
+  ["scheme"]=>
+  NULL
+  ["username"]=>
+  NULL
+  ["password"]=>
+  NULL
+  ["host"]=>
+  NULL
+  ["port"]=>
+  NULL
+  ["path"]=>
+  string(0) ""
+  ["query"]=>
+  NULL
+  ["fragment"]=>
+  string(3) "foo"
+}
+bool(true)
diff --git a/ext/uri/tests/rfc3986/builder/fragment_success_null.phpt b/ext/uri/tests/rfc3986/builder/fragment_success_null.phpt
new file mode 100644
index 00000000000..69fc4d6bca4
--- /dev/null
+++ b/ext/uri/tests/rfc3986/builder/fragment_success_null.phpt
@@ -0,0 +1,36 @@
+--TEST--
+Test Uri\Rfc3986\UriBuilder::setFragment() - success - null
+--FILE--
+<?php
+
+$builder = new Uri\Rfc3986\UriBuilder();
+$builder->setFragment("foo");
+$builder->setFragment(null);
+$uri = $builder->build();
+
+var_dump($uri->toRawString());
+var_dump($uri);
+var_dump($uri->equals(new Uri\Rfc3986\Uri($uri->toRawString())));
+
+?>
+--EXPECTF--
+string(0) ""
+object(Uri\Rfc3986\Uri)#%d (%d) {
+  ["scheme"]=>
+  NULL
+  ["username"]=>
+  NULL
+  ["password"]=>
+  NULL
+  ["host"]=>
+  NULL
+  ["port"]=>
+  NULL
+  ["path"]=>
+  string(0) ""
+  ["query"]=>
+  NULL
+  ["fragment"]=>
+  NULL
+}
+bool(true)
diff --git a/ext/uri/tests/rfc3986/builder/host_error_ipv6_closing_brace.phpt b/ext/uri/tests/rfc3986/builder/host_error_ipv6_closing_brace.phpt
new file mode 100644
index 00000000000..45eef13faf7
--- /dev/null
+++ b/ext/uri/tests/rfc3986/builder/host_error_ipv6_closing_brace.phpt
@@ -0,0 +1,16 @@
+--TEST--
+Test Uri\Rfc3986\UriBuilder::setHost() - error - missing IPv6 closing brace
+--FILE--
+<?php
+
+$builder = new Uri\Rfc3986\UriBuilder();
+
+try {
+    $builder->setHost("[2001:%30db8:85a3:0000:0000:8a2e:0370:7334");
+} catch (Throwable $e) {
+    echo $e::class, ": ", $e->getMessage(), PHP_EOL;
+}
+
+?>
+--EXPECT--
+Uri\InvalidUriException: The specified host is malformed
diff --git a/ext/uri/tests/rfc3986/builder/host_error_percent_encoding1.phpt b/ext/uri/tests/rfc3986/builder/host_error_percent_encoding1.phpt
new file mode 100644
index 00000000000..ae06238a1f8
--- /dev/null
+++ b/ext/uri/tests/rfc3986/builder/host_error_percent_encoding1.phpt
@@ -0,0 +1,16 @@
+--TEST--
+Test Uri\Rfc3986\UriBuilder::setHost() - error - invalid percent encoding octet in registered name
+--FILE--
+<?php
+
+$builder = new Uri\Rfc3986\UriBuilder();
+
+try {
+    $builder->setHost("ex%3mple.co");
+} catch (Throwable $e) {
+    echo $e::class, ": ", $e->getMessage(), PHP_EOL;
+}
+
+?>
+--EXPECT--
+Uri\InvalidUriException: The specified host is malformed
diff --git a/ext/uri/tests/rfc3986/builder/host_error_percent_encoding2.phpt b/ext/uri/tests/rfc3986/builder/host_error_percent_encoding2.phpt
new file mode 100644
index 00000000000..cc514ec6919
--- /dev/null
+++ b/ext/uri/tests/rfc3986/builder/host_error_percent_encoding2.phpt
@@ -0,0 +1,16 @@
+--TEST--
+Test Uri\Rfc3986\UriBuilder::setHost() - error - invalid percent encoded octet in IPv6
+--FILE--
+<?php
+
+$builder = new Uri\Rfc3986\UriBuilder();
+
+try {
+    $builder->setHost("[2001:%308:85a3:0000:0000:8a2e:0370:7334]");
+} catch (Throwable $e) {
+    echo $e::class, ": ", $e->getMessage(), PHP_EOL;
+}
+
+?>
+--EXPECT--
+Uri\InvalidUriException: The specified host is malformed
diff --git a/ext/uri/tests/rfc3986/builder/host_success_ip4_to_regname.phpt b/ext/uri/tests/rfc3986/builder/host_success_ip4_to_regname.phpt
new file mode 100644
index 00000000000..561a66c4037
--- /dev/null
+++ b/ext/uri/tests/rfc3986/builder/host_success_ip4_to_regname.phpt
@@ -0,0 +1,37 @@
+--TEST--
+Test Uri\Rfc3986\UriBuilder::setHost() - success - invalid IPv4 address falls back to a registered name
+--FILE--
+<?php
+
+$builder = new Uri\Rfc3986\UriBuilder();
+$builder->setHost("192.168.%30.1");
+$uri = $builder->build();
+
+var_dump($uri->toRawString());
+var_dump($uri->getHostType());
+var_dump($uri);
+var_dump($uri->equals(new Uri\Rfc3986\Uri($uri->toRawString())));
+
+?>
+--EXPECTF--
+string(15) "//192.168.%30.1"
+enum(Uri\Rfc3986\UriHostType::RegisteredName)
+object(Uri\Rfc3986\Uri)#%d (%d) {
+  ["scheme"]=>
+  NULL
+  ["username"]=>
+  NULL
+  ["password"]=>
+  NULL
+  ["host"]=>
+  string(13) "192.168.%30.1"
+  ["port"]=>
+  NULL
+  ["path"]=>
+  string(0) ""
+  ["query"]=>
+  NULL
+  ["fragment"]=>
+  NULL
+}
+bool(true)
diff --git a/ext/uri/tests/rfc3986/builder/host_success_ipv4.phpt b/ext/uri/tests/rfc3986/builder/host_success_ipv4.phpt
new file mode 100644
index 00000000000..7ae3b901085
--- /dev/null
+++ b/ext/uri/tests/rfc3986/builder/host_success_ipv4.phpt
@@ -0,0 +1,35 @@
+--TEST--
+Test Uri\Rfc3986\UriBuilder::setHost() - success - IPv4 address
+--FILE--
+<?php
+
+$builder = new Uri\Rfc3986\UriBuilder();
+$builder->setHost("192.168.0.1");
+$uri = $builder->build();
+
+var_dump($uri->toRawString());
+var_dump($uri);
+var_dump($uri->equals(new Uri\Rfc3986\Uri($uri->toRawString())));
+
+?>
+--EXPECTF--
+string(13) "//192.168.0.1"
+object(Uri\Rfc3986\Uri)#%d (%d) {
+  ["scheme"]=>
+  NULL
+  ["username"]=>
+  NULL
+  ["password"]=>
+  NULL
+  ["host"]=>
+  string(11) "192.168.0.1"
+  ["port"]=>
+  NULL
+  ["path"]=>
+  string(0) ""
+  ["query"]=>
+  NULL
+  ["fragment"]=>
+  NULL
+}
+bool(true)
diff --git a/ext/uri/tests/rfc3986/builder/host_success_ipv6.phpt b/ext/uri/tests/rfc3986/builder/host_success_ipv6.phpt
new file mode 100644
index 00000000000..f0216f7e7e0
--- /dev/null
+++ b/ext/uri/tests/rfc3986/builder/host_success_ipv6.phpt
@@ -0,0 +1,35 @@
+--TEST--
+Test Uri\Rfc3986\UriBuilder::setHost() - success - IPv6 address
+--FILE--
+<?php
+
+$builder = new Uri\Rfc3986\UriBuilder();
+$builder->setHost("[2001:0db8:85a3:0000:0000:8a2e:0370:7334]");
+$uri = $builder->build();
+
+var_dump($uri->toRawString());
+var_dump($uri);
+var_dump($uri->equals(new Uri\Rfc3986\Uri($uri->toRawString())));
+
+?>
+--EXPECTF--
+string(43) "//[2001:0db8:85a3:0000:0000:8a2e:0370:7334]"
+object(Uri\Rfc3986\Uri)#%d (%d) {
+  ["scheme"]=>
+  NULL
+  ["username"]=>
+  NULL
+  ["password"]=>
+  NULL
+  ["host"]=>
+  string(41) "[2001:0db8:85a3:0000:0000:8a2e:0370:7334]"
+  ["port"]=>
+  NULL
+  ["path"]=>
+  string(0) ""
+  ["query"]=>
+  NULL
+  ["fragment"]=>
+  NULL
+}
+bool(true)
diff --git a/ext/uri/tests/rfc3986/builder/host_success_ipvfuture.phpt b/ext/uri/tests/rfc3986/builder/host_success_ipvfuture.phpt
new file mode 100644
index 00000000000..470ef1da97d
--- /dev/null
+++ b/ext/uri/tests/rfc3986/builder/host_success_ipvfuture.phpt
@@ -0,0 +1,35 @@
+--TEST--
+Test Uri\Rfc3986\UriBuilder::setHost() - success - IPvFuture address
+--FILE--
+<?php
+
+$builder = new Uri\Rfc3986\UriBuilder();
+$builder->setHost("[v1.2001:db8::1]");
+$uri = $builder->build();
+
+var_dump($uri->toRawString());
+var_dump($uri);
+var_dump($uri->equals(new Uri\Rfc3986\Uri($uri->toRawString())));
+
+?>
+--EXPECTF--
+string(18) "//[v1.2001:db8::1]"
+object(Uri\Rfc3986\Uri)#%d (%d) {
+  ["scheme"]=>
+  NULL
+  ["username"]=>
+  NULL
+  ["password"]=>
+  NULL
+  ["host"]=>
+  string(16) "[v1.2001:db8::1]"
+  ["port"]=>
+  NULL
+  ["path"]=>
+  string(0) ""
+  ["query"]=>
+  NULL
+  ["fragment"]=>
+  NULL
+}
+bool(true)
diff --git a/ext/uri/tests/rfc3986/builder/host_success_null.phpt b/ext/uri/tests/rfc3986/builder/host_success_null.phpt
new file mode 100644
index 00000000000..87217d0ef28
--- /dev/null
+++ b/ext/uri/tests/rfc3986/builder/host_success_null.phpt
@@ -0,0 +1,34 @@
+--TEST--
+Test Uri\Rfc3986\UriBuilder::setHost() - success - null
+--FILE--
+<?php
+
+$builder = new Uri\Rfc3986\UriBuilder();
+$builder->setHost("example.com");
+$builder->setHost(null);
+$uri = $builder->build();
+
+var_dump($uri->toRawString());
+var_dump($uri);
+
+?>
+--EXPECTF--
+string(0) ""
+object(Uri\Rfc3986\Uri)#%d (%d) {
+  ["scheme"]=>
+  NULL
+  ["username"]=>
+  NULL
+  ["password"]=>
+  NULL
+  ["host"]=>
+  NULL
+  ["port"]=>
+  NULL
+  ["path"]=>
+  string(0) ""
+  ["query"]=>
+  NULL
+  ["fragment"]=>
+  NULL
+}
diff --git a/ext/uri/tests/rfc3986/builder/host_success_regname.phpt b/ext/uri/tests/rfc3986/builder/host_success_regname.phpt
new file mode 100644
index 00000000000..81e93ccfc10
--- /dev/null
+++ b/ext/uri/tests/rfc3986/builder/host_success_regname.phpt
@@ -0,0 +1,35 @@
+--TEST--
+Test Uri\Rfc3986\UriBuilder::setHost() - success - Registered name
+--FILE--
+<?php
+
+$builder = new Uri\Rfc3986\UriBuilder();
+$builder->setHost("www.example.com");
+$uri = $builder->build();
+
+var_dump($uri->toRawString());
+var_dump($uri);
+var_dump($uri->equals(new Uri\Rfc3986\Uri($uri->toRawString())));
+
+?>
+--EXPECTF--
+string(17) "//www.example.com"
+object(Uri\Rfc3986\Uri)#%d (%d) {
+  ["scheme"]=>
+  NULL
+  ["username"]=>
+  NULL
+  ["password"]=>
+  NULL
+  ["host"]=>
+  string(15) "www.example.com"
+  ["port"]=>
+  NULL
+  ["path"]=>
+  string(0) ""
+  ["query"]=>
+  NULL
+  ["fragment"]=>
+  NULL
+}
+bool(true)
diff --git a/ext/uri/tests/rfc3986/builder/path_error_first_segment_colon.phpt b/ext/uri/tests/rfc3986/builder/path_error_first_segment_colon.phpt
new file mode 100644
index 00000000000..8cce8d471aa
--- /dev/null
+++ b/ext/uri/tests/rfc3986/builder/path_error_first_segment_colon.phpt
@@ -0,0 +1,17 @@
+--TEST--
+Test Uri\Rfc3986\UriBuilder::setPath() - error - contains a colon in the first path segment when the URI doesn't contain a scheme
+--FILE--
+<?php
+
+$builder = new Uri\Rfc3986\UriBuilder();
+$builder->setPath("fo:o/bar/baz");
+
+try {
+    $builder->build();
+} catch (Throwable $e) {
+    echo $e::class, ": ", $e->getMessage(), PHP_EOL;
+}
+
+?>
+--EXPECT--
+Uri\InvalidUriException: The path must not begin with ":" when the URI does not contain a scheme
diff --git a/ext/uri/tests/rfc3986/builder/path_error_leading_double_slash.phpt b/ext/uri/tests/rfc3986/builder/path_error_leading_double_slash.phpt
new file mode 100644
index 00000000000..b1f0a2518f1
--- /dev/null
+++ b/ext/uri/tests/rfc3986/builder/path_error_leading_double_slash.phpt
@@ -0,0 +1,17 @@
+--TEST--
+Test Uri\Rfc3986\UriBuilder::setPath() - error - contains leading double slash when the host is not present
+--FILE--
+<?php
+
+$builder = new Uri\Rfc3986\UriBuilder();
+$builder->setPath("//foo/bar/baz");
+
+try {
+    $builder->build();
+} catch (Throwable $e) {
+    echo $e::class, ": ", $e->getMessage(), PHP_EOL;
+}
+
+?>
+--EXPECT--
+Uri\InvalidUriException: The path must not begin with "//" when the URI does not contain a host
diff --git a/ext/uri/tests/rfc3986/builder/path_error_missing_leading_slash.phpt b/ext/uri/tests/rfc3986/builder/path_error_missing_leading_slash.phpt
new file mode 100644
index 00000000000..97b6c3e8b85
--- /dev/null
+++ b/ext/uri/tests/rfc3986/builder/path_error_missing_leading_slash.phpt
@@ -0,0 +1,18 @@
+--TEST--
+Test Uri\Rfc3986\UriBuilder::setPath() - error - missing a leading slash when the URI contains a host
+--FILE--
+<?php
+
+$builder = new Uri\Rfc3986\UriBuilder();
+$builder->setPath("foo/bar/baz");
+$builder->setHost("example.com");
+
+try {
+    $builder->build();
+} catch (Throwable $e) {
+    echo $e::class, ": ", $e->getMessage(), PHP_EOL;
+}
+
+?>
+--EXPECT--
+Uri\InvalidUriException: The specified path is malformed
diff --git a/ext/uri/tests/rfc3986/builder/path_error_special_char.phpt b/ext/uri/tests/rfc3986/builder/path_error_special_char.phpt
new file mode 100644
index 00000000000..039b2277024
--- /dev/null
+++ b/ext/uri/tests/rfc3986/builder/path_error_special_char.phpt
@@ -0,0 +1,16 @@
+--TEST--
+Test Uri\Rfc3986\UriBuilder::setPath() - error - contains invalid special character
+--FILE--
+<?php
+
+$builder = new Uri\Rfc3986\UriBuilder();
+
+try {
+    $builder->setPath("#foo");
+} catch (Throwable $e) {
+    echo $e::class, ": ", $e->getMessage(), PHP_EOL;
+}
+
+?>
+--EXPECT--
+Uri\InvalidUriException: The specified path is malformed
diff --git a/ext/uri/tests/rfc3986/builder/path_success_empty_string.phpt b/ext/uri/tests/rfc3986/builder/path_success_empty_string.phpt
new file mode 100644
index 00000000000..00ed0c95355
--- /dev/null
+++ b/ext/uri/tests/rfc3986/builder/path_success_empty_string.phpt
@@ -0,0 +1,35 @@
+--TEST--
+Test Uri\Rfc3986\UriBuilder::setPath() - success - empty string
+--FILE--
+<?php
+
+$builder = new Uri\Rfc3986\UriBuilder();
+$builder->setPath("");
+$uri = $builder->build();
+
+var_dump($uri->toRawString());
+var_dump($uri);
+var_dump($uri->equals(new Uri\Rfc3986\Uri($uri->toRawString())));
+
+?>
+--EXPECTF--
+string(0) ""
+object(Uri\Rfc3986\Uri)#%d (%d) {
+  ["scheme"]=>
+  NULL
+  ["username"]=>
+  NULL
+  ["password"]=>
+  NULL
+  ["host"]=>
+  NULL
+  ["port"]=>
+  NULL
+  ["path"]=>
+  string(0) ""
+  ["query"]=>
+  NULL
+  ["fragment"]=>
+  NULL
+}
+bool(true)
diff --git a/ext/uri/tests/rfc3986/builder/path_success_first_segment_colon.phpt b/ext/uri/tests/rfc3986/builder/path_success_first_segment_colon.phpt
new file mode 100644
index 00000000000..e1fc689ba2f
--- /dev/null
+++ b/ext/uri/tests/rfc3986/builder/path_success_first_segment_colon.phpt
@@ -0,0 +1,36 @@
+--TEST--
+Test Uri\Rfc3986\UriBuilder::setPath() - success - contains a colon in the first segment when the scheme is present
+--FILE--
+<?php
+
+$builder = new Uri\Rfc3986\UriBuilder();
+$builder->setScheme("https");
+$builder->setPath(":foo/bar/baz");
+$uri = $builder->build();
+
+var_dump($uri->toRawString());
+var_dump($uri);
+var_dump($uri->equals(new Uri\Rfc3986\Uri($uri->toRawString())));
+
+?>
+--EXPECTF--
+string(%d) "https::foo/bar/baz"
+object(Uri\Rfc3986\Uri)#%d (%d) {
+  ["scheme"]=>
+  string(5) "https"
+  ["username"]=>
+  NULL
+  ["password"]=>
+  NULL
+  ["host"]=>
+  NULL
+  ["port"]=>
+  NULL
+  ["path"]=>
+  string(12) ":foo/bar/baz"
+  ["query"]=>
+  NULL
+  ["fragment"]=>
+  NULL
+}
+bool(true)
diff --git a/ext/uri/tests/rfc3986/builder/path_success_leading_double_slash.phpt b/ext/uri/tests/rfc3986/builder/path_success_leading_double_slash.phpt
new file mode 100644
index 00000000000..d00fc87b440
--- /dev/null
+++ b/ext/uri/tests/rfc3986/builder/path_success_leading_double_slash.phpt
@@ -0,0 +1,36 @@
+--TEST--
+Test Uri\Rfc3986\UriBuilder::setPath() - success - begins with double slashes when the URI contains a host
+--FILE--
+<?php
+
+$builder = new Uri\Rfc3986\UriBuilder();
+$builder->setHost("example.com");
+$builder->setPath("//foo/bar/baz");
+$uri = $builder->build();
+
+var_dump($uri->toRawString());
+var_dump($uri);
+var_dump($uri->equals(new Uri\Rfc3986\Uri($uri->toRawString())));
+
+?>
+--EXPECTF--
+string(26) "//example.com//foo/bar/baz"
+object(Uri\Rfc3986\Uri)#%d (%d) {
+  ["scheme"]=>
+  NULL
+  ["username"]=>
+  NULL
+  ["password"]=>
+  NULL
+  ["host"]=>
+  string(11) "example.com"
+  ["port"]=>
+  NULL
+  ["path"]=>
+  string(13) "//foo/bar/baz"
+  ["query"]=>
+  NULL
+  ["fragment"]=>
+  NULL
+}
+bool(true)
diff --git a/ext/uri/tests/rfc3986/builder/port_error_missing_host.phpt b/ext/uri/tests/rfc3986/builder/port_error_missing_host.phpt
new file mode 100644
index 00000000000..4b40e84c78f
--- /dev/null
+++ b/ext/uri/tests/rfc3986/builder/port_error_missing_host.phpt
@@ -0,0 +1,17 @@
+--TEST--
+Test Uri\Rfc3986\UriBuilder::setPort() - error - missing host
+--FILE--
+<?php
+
+$builder = new Uri\Rfc3986\UriBuilder();
+$builder->setPort(443);
+
+try {
+    $builder->build();
+} catch (Throwable $e) {
+    echo $e::class, ": ", $e->getMessage(), PHP_EOL;
+}
+
+?>
+--EXPECT--
+Uri\InvalidUriException: Cannot set a port without having a host
diff --git a/ext/uri/tests/rfc3986/builder/port_error_negative.phpt b/ext/uri/tests/rfc3986/builder/port_error_negative.phpt
new file mode 100644
index 00000000000..4cbc0e1c69d
--- /dev/null
+++ b/ext/uri/tests/rfc3986/builder/port_error_negative.phpt
@@ -0,0 +1,16 @@
+--TEST--
+Test Uri\Rfc3986\UriBuilder::setPort() - error - negative number
+--FILE--
+<?php
+
+$builder = new Uri\Rfc3986\UriBuilder();
+
+try {
+    $builder->setPort(-1);
+} catch (Throwable $e) {
+    echo $e::class, ": ", $e->getMessage(), PHP_EOL;
+}
+
+?>
+--EXPECT--
+Uri\InvalidUriException: The specified port is malformed
diff --git a/ext/uri/tests/rfc3986/builder/port_success_large.phpt b/ext/uri/tests/rfc3986/builder/port_success_large.phpt
new file mode 100644
index 00000000000..14d252a7d9c
--- /dev/null
+++ b/ext/uri/tests/rfc3986/builder/port_success_large.phpt
@@ -0,0 +1,36 @@
+--TEST--
+Test Uri\Rfc3986\UriBuilder::setPort() - success - large number
+--FILE--
+<?php
+
+$builder = new Uri\Rfc3986\UriBuilder();
+$builder->setPort(PHP_INT_MAX);
+$builder->setHost("example.com");
+$uri = $builder->build();
+
+var_dump($uri->toRawString());
+var_dump($uri);
+var_dump($uri->equals(new Uri\Rfc3986\Uri($uri->toRawString())));
+
+?>
+--EXPECTF--
+string(%d) "//example.com:%d"
+object(Uri\Rfc3986\Uri)#%d (%d) {
+  ["scheme"]=>
+  NULL
+  ["username"]=>
+  NULL
+  ["password"]=>
+  NULL
+  ["host"]=>
+  string(11) "example.com"
+  ["port"]=>
+  int(%d)
+  ["path"]=>
+  string(0) ""
+  ["query"]=>
+  NULL
+  ["fragment"]=>
+  NULL
+}
+bool(true)
diff --git a/ext/uri/tests/rfc3986/builder/port_success_null.phpt b/ext/uri/tests/rfc3986/builder/port_success_null.phpt
new file mode 100644
index 00000000000..6c2ffaf7109
--- /dev/null
+++ b/ext/uri/tests/rfc3986/builder/port_success_null.phpt
@@ -0,0 +1,36 @@
+--TEST--
+Test Uri\Rfc3986\UriBuilder::setPort() - success - null
+--FILE--
+<?php
+
+$builder = new Uri\Rfc3986\UriBuilder();
+$builder->setPort(433);
+$builder->setPort(null);
+$uri = $builder->build();
+
+var_dump($uri->toRawString());
+var_dump($uri);
+var_dump($uri->equals(new Uri\Rfc3986\Uri($uri->toRawString())));
+
+?>
+--EXPECTF--
+string(0) ""
+object(Uri\Rfc3986\Uri)#%d (%d) {
+  ["scheme"]=>
+  NULL
+  ["username"]=>
+  NULL
+  ["password"]=>
+  NULL
+  ["host"]=>
+  NULL
+  ["port"]=>
+  NULL
+  ["path"]=>
+  string(0) ""
+  ["query"]=>
+  NULL
+  ["fragment"]=>
+  NULL
+}
+bool(true)
diff --git a/ext/uri/tests/rfc3986/builder/query_error_special_char.phpt b/ext/uri/tests/rfc3986/builder/query_error_special_char.phpt
new file mode 100644
index 00000000000..ecfced24a44
--- /dev/null
+++ b/ext/uri/tests/rfc3986/builder/query_error_special_char.phpt
@@ -0,0 +1,16 @@
+--TEST--
+Test Uri\Rfc3986\UriBuilder::setQuery() - error - contains invalid special character
+--FILE--
+<?php
+
+$builder = new Uri\Rfc3986\UriBuilder();
+
+try {
+    $builder->setQuery("#foo");
+} catch (Throwable $e) {
+    echo $e::class, ": ", $e->getMessage(), PHP_EOL;
+}
+
+?>
+--EXPECT--
+Uri\InvalidUriException: The specified query is malformed
diff --git a/ext/uri/tests/rfc3986/builder/query_error_unicode_char.phpt b/ext/uri/tests/rfc3986/builder/query_error_unicode_char.phpt
new file mode 100644
index 00000000000..4c212cddd63
--- /dev/null
+++ b/ext/uri/tests/rfc3986/builder/query_error_unicode_char.phpt
@@ -0,0 +1,16 @@
+--TEST--
+Test Uri\Rfc3986\UriBuilder::setQuery() - error - contains Unicode character
+--FILE--
+<?php
+
+$builder = new Uri\Rfc3986\UriBuilder();
+
+try {
+    $builder->setQuery("főő");
+} catch (Throwable $e) {
+    echo $e::class, ": ", $e->getMessage(), PHP_EOL;
+}
+
+?>
+--EXPECT--
+Uri\InvalidUriException: The specified query is malformed
diff --git a/ext/uri/tests/rfc3986/builder/query_success_basic.phpt b/ext/uri/tests/rfc3986/builder/query_success_basic.phpt
new file mode 100644
index 00000000000..423b03b0b01
--- /dev/null
+++ b/ext/uri/tests/rfc3986/builder/query_success_basic.phpt
@@ -0,0 +1,35 @@
+--TEST--
+Test Uri\Rfc3986\UriBuilder::setQuery() - success - basic
+--FILE--
+<?php
+
+$builder = new Uri\Rfc3986\UriBuilder();
+$builder->setQuery("foo=1&bar=baz");
+$uri = $builder->build();
+
+var_dump($uri->toRawString());
+var_dump($uri);
+var_dump($uri->equals(new Uri\Rfc3986\Uri($uri->toRawString())));
+
+?>
+--EXPECTF--
+string(14) "?foo=1&bar=baz"
+object(Uri\Rfc3986\Uri)#%d (%d) {
+  ["scheme"]=>
+  NULL
+  ["username"]=>
+  NULL
+  ["password"]=>
+  NULL
+  ["host"]=>
+  NULL
+  ["port"]=>
+  NULL
+  ["path"]=>
+  string(0) ""
+  ["query"]=>
+  string(13) "foo=1&bar=baz"
+  ["fragment"]=>
+  NULL
+}
+bool(true)
diff --git a/ext/uri/tests/rfc3986/builder/query_success_null.phpt b/ext/uri/tests/rfc3986/builder/query_success_null.phpt
new file mode 100644
index 00000000000..8e233c02c84
--- /dev/null
+++ b/ext/uri/tests/rfc3986/builder/query_success_null.phpt
@@ -0,0 +1,36 @@
+--TEST--
+Test Uri\Rfc3986\UriBuilder::setQuery() - success - null
+--FILE--
+<?php
+
+$builder = new Uri\Rfc3986\UriBuilder();
+$builder->setQuery("foo");
+$builder->setQuery(null);
+$uri = $builder->build();
+
+var_dump($uri->toRawString());
+var_dump($uri);
+var_dump($uri->equals(new Uri\Rfc3986\Uri($uri->toRawString())));
+
+?>
+--EXPECTF--
+string(0) ""
+object(Uri\Rfc3986\Uri)#%d (%d) {
+  ["scheme"]=>
+  NULL
+  ["username"]=>
+  NULL
+  ["password"]=>
+  NULL
+  ["host"]=>
+  NULL
+  ["port"]=>
+  NULL
+  ["path"]=>
+  string(0) ""
+  ["query"]=>
+  NULL
+  ["fragment"]=>
+  NULL
+}
+bool(true)
diff --git a/ext/uri/tests/rfc3986/builder/scheme_error_empty.phpt b/ext/uri/tests/rfc3986/builder/scheme_error_empty.phpt
new file mode 100644
index 00000000000..13594c16336
--- /dev/null
+++ b/ext/uri/tests/rfc3986/builder/scheme_error_empty.phpt
@@ -0,0 +1,16 @@
+--TEST--
+Test Uri\Rfc3986\UriBuilder::setScheme() - error - empty
+--FILE--
+<?php
+
+$builder = new Uri\Rfc3986\UriBuilder();
+
+try {
+    $builder->setScheme("");
+} catch (Throwable $e) {
+    echo $e::class, ": ", $e->getMessage(), PHP_EOL;
+}
+
+?>
+--EXPECT--
+Uri\InvalidUriException: The specified scheme is malformed
diff --git a/ext/uri/tests/rfc3986/builder/scheme_error_first_char.phpt b/ext/uri/tests/rfc3986/builder/scheme_error_first_char.phpt
new file mode 100644
index 00000000000..c178a0fa1f1
--- /dev/null
+++ b/ext/uri/tests/rfc3986/builder/scheme_error_first_char.phpt
@@ -0,0 +1,16 @@
+--TEST--
+Test Uri\Rfc3986\UriBuilder::setScheme() - error - first character is not alpha
+--FILE--
+<?php
+
+$builder = new Uri\Rfc3986\UriBuilder();
+
+try {
+    $builder->setScheme("1");
+} catch (Throwable $e) {
+    echo $e::class, ": ", $e->getMessage(), PHP_EOL;
+}
+
+?>
+--EXPECT--
+Uri\InvalidUriException: The specified scheme is malformed
diff --git a/ext/uri/tests/rfc3986/builder/scheme_error_special_char.phpt b/ext/uri/tests/rfc3986/builder/scheme_error_special_char.phpt
new file mode 100644
index 00000000000..4941db44aa7
--- /dev/null
+++ b/ext/uri/tests/rfc3986/builder/scheme_error_special_char.phpt
@@ -0,0 +1,16 @@
+--TEST--
+Test Uri\Rfc3986\UriBuilder::setScheme() - error - contains invalid special character
+--FILE--
+<?php
+
+$builder = new Uri\Rfc3986\UriBuilder();
+
+try {
+    $builder->setScheme(":");
+} catch (Throwable $e) {
+    echo $e::class, ": ", $e->getMessage(), PHP_EOL;
+}
+
+?>
+--EXPECT--
+Uri\InvalidUriException: The specified scheme is malformed
diff --git a/ext/uri/tests/rfc3986/builder/scheme_success_basic.phpt b/ext/uri/tests/rfc3986/builder/scheme_success_basic.phpt
new file mode 100644
index 00000000000..279ecb27362
--- /dev/null
+++ b/ext/uri/tests/rfc3986/builder/scheme_success_basic.phpt
@@ -0,0 +1,35 @@
+--TEST--
+Test Uri\Rfc3986\UriBuilder::setScheme() - success - basic
+--FILE--
+<?php
+
+$builder = new Uri\Rfc3986\UriBuilder();
+$builder->setScheme("scheme");
+$uri = $builder->build();
+
+var_dump($uri->toRawString());
+var_dump($uri);
+var_dump($uri->equals(new Uri\Rfc3986\Uri($uri->toRawString())));
+
+?>
+--EXPECTF--
+string(7) "scheme:"
+object(Uri\Rfc3986\Uri)#%d (%d) {
+  ["scheme"]=>
+  string(6) "scheme"
+  ["username"]=>
+  NULL
+  ["password"]=>
+  NULL
+  ["host"]=>
+  NULL
+  ["port"]=>
+  NULL
+  ["path"]=>
+  string(0) ""
+  ["query"]=>
+  NULL
+  ["fragment"]=>
+  NULL
+}
+bool(true)
diff --git a/ext/uri/tests/rfc3986/builder/scheme_success_null.phpt b/ext/uri/tests/rfc3986/builder/scheme_success_null.phpt
new file mode 100644
index 00000000000..1a9510156b0
--- /dev/null
+++ b/ext/uri/tests/rfc3986/builder/scheme_success_null.phpt
@@ -0,0 +1,36 @@
+--TEST--
+Test Uri\Rfc3986\UriBuilder::setScheme() - success - null
+--FILE--
+<?php
+
+$builder = new Uri\Rfc3986\UriBuilder();
+$builder->setScheme("https");
+$builder->setScheme(null);
+$uri = $builder->build();
+
+var_dump($uri->toRawString());
+var_dump($uri);
+var_dump($uri->equals(new Uri\Rfc3986\Uri($uri->toRawString())));
+
+?>
+--EXPECTF--
+string(0) ""
+object(Uri\Rfc3986\Uri)#%d (%d) {
+  ["scheme"]=>
+  NULL
+  ["username"]=>
+  NULL
+  ["password"]=>
+  NULL
+  ["host"]=>
+  NULL
+  ["port"]=>
+  NULL
+  ["path"]=>
+  string(0) ""
+  ["query"]=>
+  NULL
+  ["fragment"]=>
+  NULL
+}
+bool(true)
diff --git a/ext/uri/tests/rfc3986/builder/scheme_success_special.phpt b/ext/uri/tests/rfc3986/builder/scheme_success_special.phpt
new file mode 100644
index 00000000000..f387bc09f2e
--- /dev/null
+++ b/ext/uri/tests/rfc3986/builder/scheme_success_special.phpt
@@ -0,0 +1,35 @@
+--TEST--
+Test Uri\Rfc3986\UriBuilder::setScheme() - success - contains digit & special characters
+--FILE--
+<?php
+
+$builder = new Uri\Rfc3986\UriBuilder();
+$builder->setScheme("my-12+34.scheme");
+$uri = $builder->build();
+
+var_dump($uri->toRawString());
+var_dump($uri);
+var_dump($uri->equals(new Uri\Rfc3986\Uri($uri->toRawString())));
+
+?>
+--EXPECTF--
+string(16) "my-12+34.scheme:"
+object(Uri\Rfc3986\Uri)#%d (%d) {
+  ["scheme"]=>
+  string(15) "my-12+34.scheme"
+  ["username"]=>
+  NULL
+  ["password"]=>
+  NULL
+  ["host"]=>
+  NULL
+  ["port"]=>
+  NULL
+  ["path"]=>
+  string(0) ""
+  ["query"]=>
+  NULL
+  ["fragment"]=>
+  NULL
+}
+bool(true)
diff --git a/ext/uri/tests/rfc3986/builder/userinfo_error_missing_host.phpt b/ext/uri/tests/rfc3986/builder/userinfo_error_missing_host.phpt
new file mode 100644
index 00000000000..a6919e8ffd0
--- /dev/null
+++ b/ext/uri/tests/rfc3986/builder/userinfo_error_missing_host.phpt
@@ -0,0 +1,17 @@
+--TEST--
+Test Uri\Rfc3986\UriBuilder::setUserInfo() - error - missing host
+--FILE--
+<?php
+
+$builder = new Uri\Rfc3986\UriBuilder();
+$builder->setUserInfo("user:pass");
+
+try {
+    $builder->build();
+} catch (Throwable $e) {
+    echo $e::class, ": ", $e->getMessage(), PHP_EOL;
+}
+
+?>
+--EXPECT--
+Uri\InvalidUriException: Cannot set a userinfo without having a host
diff --git a/ext/uri/tests/rfc3986/builder/userinfo_error_percent_encoding.phpt b/ext/uri/tests/rfc3986/builder/userinfo_error_percent_encoding.phpt
new file mode 100644
index 00000000000..0830a57864d
--- /dev/null
+++ b/ext/uri/tests/rfc3986/builder/userinfo_error_percent_encoding.phpt
@@ -0,0 +1,16 @@
+--TEST--
+Test Uri\Rfc3986\UriBuilder::setUserInfo() - error - invalid percent encoding
+--FILE--
+<?php
+
+$builder = new Uri\Rfc3986\UriBuilder();
+
+try {
+    $builder->setUserInfo("%3");
+} catch (Throwable $e) {
+    echo $e::class, ": ", $e->getMessage(), PHP_EOL;
+}
+
+?>
+--EXPECT--
+Uri\InvalidUriException: The specified userinfo is malformed
diff --git a/ext/uri/tests/rfc3986/builder/userinfo_error_special_char.phpt b/ext/uri/tests/rfc3986/builder/userinfo_error_special_char.phpt
new file mode 100644
index 00000000000..9d3fb2544da
--- /dev/null
+++ b/ext/uri/tests/rfc3986/builder/userinfo_error_special_char.phpt
@@ -0,0 +1,16 @@
+--TEST--
+Test Uri\Rfc3986\UriBuilder::setUserInfo() - error - contains invalid special character
+--FILE--
+<?php
+
+$builder = new Uri\Rfc3986\UriBuilder();
+
+try {
+    $builder->setUserInfo("<>");
+} catch (Throwable $e) {
+    echo $e::class, ": ", $e->getMessage(), PHP_EOL;
+}
+
+?>
+--EXPECT--
+Uri\InvalidUriException: The specified userinfo is malformed
diff --git a/ext/uri/tests/rfc3986/builder/userinfo_success_null.phpt b/ext/uri/tests/rfc3986/builder/userinfo_success_null.phpt
new file mode 100644
index 00000000000..3decca97e79
--- /dev/null
+++ b/ext/uri/tests/rfc3986/builder/userinfo_success_null.phpt
@@ -0,0 +1,36 @@
+--TEST--
+Test Uri\Rfc3986\UriBuilder::setUserInfo() - success - null
+--FILE--
+<?php
+
+$builder = new Uri\Rfc3986\UriBuilder();
+$builder->setUserInfo("user:pass");
+$builder->setUserInfo(null);
+$uri = $builder->build();
+
+var_dump($uri->toRawString());
+var_dump($uri);
+var_dump($uri->equals(new Uri\Rfc3986\Uri($uri->toRawString())));
+
+?>
+--EXPECTF--
+string(0) ""
+object(Uri\Rfc3986\Uri)#%d (%d) {
+  ["scheme"]=>
+  NULL
+  ["username"]=>
+  NULL
+  ["password"]=>
+  NULL
+  ["host"]=>
+  NULL
+  ["port"]=>
+  NULL
+  ["path"]=>
+  string(0) ""
+  ["query"]=>
+  NULL
+  ["fragment"]=>
+  NULL
+}
+bool(true)
diff --git a/ext/uri/uri_parser_rfc3986.c b/ext/uri/uri_parser_rfc3986.c
index b8a14a31c10..88201914d59 100644
--- a/ext/uri/uri_parser_rfc3986.c
+++ b/ext/uri/uri_parser_rfc3986.c
@@ -545,12 +545,35 @@ static php_uri_parser_rfc3986_uris *uriparser_create_uris(void)
 	return uriparser_uris;
 }

-php_uri_parser_rfc3986_uris *php_uri_parser_rfc3986_parse_ex(const char *uri_str, size_t uri_str_len, const php_uri_parser_rfc3986_uris *uriparser_base_urls, bool silent)
+static zend_result php_uri_parser_rfc3986_add_base_url(
+	UriUriA *tmp, const UriUriA *uri, const php_uri_parser_rfc3986_uris *uriparser_base_urls, const bool silent
+) {
+	const int result = uriAddBaseUriExMmA(tmp, uri, &uriparser_base_urls->uri, URI_RESOLVE_STRICTLY, mm);
+	if (result != URI_SUCCESS) {
+		if (!silent) {
+			switch (result) {
+				case URI_ERROR_ADDBASE_REL_BASE:
+					zend_throw_exception(php_uri_ce_invalid_uri_exception, "The specified base URI must be absolute", 0);
+					break;
+				default:
+					/* This should be unreachable in practice. */
+					zend_throw_exception(php_uri_ce_error, "Failed to resolve the specified URI against the base URI", 0);
+					break;
+			}
+		}
+
+		return FAILURE;
+	}
+
+	return SUCCESS;
+}
+
+php_uri_parser_rfc3986_uris *php_uri_parser_rfc3986_parse_ex(const char *uri_str, const size_t uri_str_len, const php_uri_parser_rfc3986_uris *uriparser_base_urls, const bool silent)
 {
 	UriUriA uri = {0};

 	/* Parse the URI. */
-	int result = uriParseSingleUriExMmA(&uri, uri_str, uri_str + uri_str_len, NULL, mm);
+	const int result = uriParseSingleUriExMmA(&uri, uri_str, uri_str + uri_str_len, NULL, mm);
 	if (result != URI_SUCCESS) {
 		if (!silent) {
 			switch (result) {
@@ -572,20 +595,7 @@ php_uri_parser_rfc3986_uris *php_uri_parser_rfc3986_parse_ex(const char *uri_str

 		/* Combine the parsed URI with the base URI and store the result in 'tmp',
 		 * since the target and source URLs must be distinct. */
-		int result = uriAddBaseUriExMmA(&tmp, &uri, &uriparser_base_urls->uri, URI_RESOLVE_STRICTLY, mm);
-		if (result != URI_SUCCESS) {
-			if (!silent) {
-				switch (result) {
-					case URI_ERROR_ADDBASE_REL_BASE:
-						zend_throw_exception(php_uri_ce_invalid_uri_exception, "The specified base URI must be absolute", 0);
-						break;
-					default:
-						/* This should be unreachable in practice. */
-						zend_throw_exception(php_uri_ce_error, "Failed to resolve the specified URI against the base URI", 0);
-						break;
-				}
-			}
-
+		if (php_uri_parser_rfc3986_add_base_url(&tmp, &uri, uriparser_base_urls, silent) == FAILURE) {
 			goto fail;
 		}

@@ -683,6 +693,189 @@ static void php_uri_parser_rfc3986_destroy(void *uri)
 	efree(uriparser_uris);
 }

+static zend_always_inline zend_result php_uri_parser_rfc3986_validate_component_result(const bool well_formed, const char *component_name)
+{
+	if (well_formed) {
+		return SUCCESS;
+	}
+
+	zend_throw_exception_ex(php_uri_ce_invalid_uri_exception, 0, "The specified %s is malformed", component_name);
+	return FAILURE;
+}
+
+ZEND_ATTRIBUTE_NONNULL zend_result php_uri_parser_rfc3986_validate_scheme(const zend_string *scheme)
+{
+	const char *p = ZSTR_VAL(scheme);
+	const size_t len = ZSTR_LEN(scheme);
+	const bool well_formed = uriIsWellFormedSchemeA(p, p + len) == URI_TRUE;
+
+	return php_uri_parser_rfc3986_validate_component_result(well_formed, "scheme");
+}
+
+ZEND_ATTRIBUTE_NONNULL zend_result php_uri_parser_rfc3986_validate_userinfo(const zend_string *userinfo)
+{
+	const char *p = ZSTR_VAL(userinfo);
+	const size_t len = ZSTR_LEN(userinfo);
+	const bool well_formed = uriIsWellFormedUserInfoA(p, p + len) == URI_TRUE;
+
+	return php_uri_parser_rfc3986_validate_component_result(well_formed, "userinfo");
+}
+
+ZEND_ATTRIBUTE_NONNULL zend_result php_uri_parser_rfc3986_validate_host(const zend_string *host)
+{
+	const char *p = ZSTR_VAL(host);
+	const size_t len = ZSTR_LEN(host);
+
+	if (len == 0) {
+		return SUCCESS;
+	}
+
+	if (p[0] == '[') {
+		if (p[len - 1] != ']') {
+			return php_uri_parser_rfc3986_validate_component_result(false, "host");
+		}
+
+		if (len >= 2 && (p[1] == 'v' || p[1] == 'V')) {
+			return php_uri_parser_rfc3986_validate_component_result(
+				uriIsWellFormedHostIpFutureA(p + 1, p + len - 1) == URI_SUCCESS,
+				"host"
+			);
+		}
+
+		return php_uri_parser_rfc3986_validate_component_result(
+			uriIsWellFormedHostIp6A(p + 1, p + len - 1) == URI_SUCCESS,
+			"host"
+		);
+	}
+
+	if (uriIsWellFormedHostIp4A(p, p + len) == URI_TRUE) {
+		return SUCCESS;
+	}
+
+	return php_uri_parser_rfc3986_validate_component_result(
+		uriIsWellFormedHostRegNameA(p, p + len) == URI_TRUE,
+		"host"
+	);
+}
+
+ZEND_ATTRIBUTE_NONNULL zend_result php_uri_parser_rfc3986_validate_port(const zend_long port)
+{
+	char buf[MAX_LENGTH_OF_LONG + 1];
+	const char *res = zend_print_long_to_buf(buf + sizeof(buf) - 1, port);
+
+	const bool well_formed = uriIsWellFormedPortA(res, res + strlen(res));
+
+	return php_uri_parser_rfc3986_validate_component_result(well_formed, "port");
+}
+
+ZEND_ATTRIBUTE_NONNULL zend_result php_uri_parser_rfc3986_validate_path(const zend_string *path)
+{
+	const char *p = ZSTR_VAL(path);
+	const size_t len = ZSTR_LEN(path);
+	/* The build() method checks whether the path begins with a "/" when there's a host.
+	 * In order to skip doing the same check, a false hasHost argument is passed to uriIsWellFormedPathA(). */
+	const bool well_formed = uriIsWellFormedPathA(p, p + len, /* hasHost */ false);
+
+	return php_uri_parser_rfc3986_validate_component_result(well_formed, "path");
+}
+
+ZEND_ATTRIBUTE_NONNULL zend_result php_uri_parser_rfc3986_validate_query(const zend_string *query)
+{
+	const char *p = ZSTR_VAL(query);
+	const size_t len = ZSTR_LEN(query);
+	const bool well_formed = uriIsWellFormedQueryA(p, p + len);
+
+	return php_uri_parser_rfc3986_validate_component_result(well_formed, "query");
+}
+
+ZEND_ATTRIBUTE_NONNULL zend_result php_uri_parser_rfc3986_validate_fragment(const zend_string *fragment)
+{
+	const char *p = ZSTR_VAL(fragment);
+	const size_t len = ZSTR_LEN(fragment);
+	const bool well_formed = uriIsWellFormedFragmentA(p, p + len);
+
+	return php_uri_parser_rfc3986_validate_component_result(well_formed, "fragment");
+}
+
+ZEND_ATTRIBUTE_NONNULL_ARGS(2,3,4,5,6,7,8) php_uri_parser_rfc3986_uris *php_uri_parser_rfc3986_build_from_zval(
+	const php_uri_parser_rfc3986_uris *uriparser_base_uris,
+	const zval *scheme, const zval *userinfo, const zval *host, const zval *port,
+	const zval *path, const zval *query, const zval *fragment
+) {
+	php_uri_parser_rfc3986_uris *uriparser_uris = uriparser_create_uris();
+
+	if (Z_STRLEN_P(path) > 0) {
+		/* The first segment of the path must not contain ":" if the URI does not contain a scheme */
+		if (Z_TYPE_P(scheme) == IS_NULL) {
+			const char *p = Z_STRVAL_P(path);
+			while (*p != '\0' && *p != '/') {
+				if (*p == ':') {
+					zend_throw_exception(php_uri_ce_invalid_uri_exception, "The path must not begin with \":\" when the URI does not contain a scheme", 0);
+					goto failure;
+				}
+
+				p++;
+			}
+		}
+
+		/* The path must not begin with "//" if the URI does not contain a host */
+		if (Z_TYPE_P(host) == IS_NULL && zend_string_starts_with_literal(Z_STR_P(path), "//")) {
+			zend_throw_exception(php_uri_ce_invalid_uri_exception, "The path must not begin with \"//\" when the URI does not contain a host", 0);
+			goto failure;
+		}
+	}
+
+	zend_result result = php_uri_parser_rfc3986_scheme_write(uriparser_uris, scheme, NULL);
+	if (result == FAILURE) {
+		goto failure;
+	}
+	result = php_uri_parser_rfc3986_host_write(uriparser_uris, host, NULL);
+	if (result == FAILURE) {
+		goto failure;
+	}
+	/* Intentionally writing userinfo after host to avoid error when the userinfo is set but the host is missing */
+	result = php_uri_parser_rfc3986_userinfo_write(uriparser_uris, userinfo, NULL);
+	if (result == FAILURE) {
+		goto failure;
+	}
+	/* Intentionally writing userinfo after host to avoid error when the port is set but the host is missing */
+	result = php_uri_parser_rfc3986_port_write(uriparser_uris, port, NULL);
+	if (result == FAILURE) {
+		goto failure;
+	}
+	result = php_uri_parser_rfc3986_path_write(uriparser_uris, path, NULL);
+	if (result == FAILURE) {
+		goto failure;
+	}
+	result = php_uri_parser_rfc3986_query_write(uriparser_uris, query, NULL);
+	if (result == FAILURE) {
+		goto failure;
+	}
+	result = php_uri_parser_rfc3986_fragment_write(uriparser_uris, fragment, NULL);
+	if (result == FAILURE) {
+		goto failure;
+	}
+
+	if (uriparser_base_uris != NULL) {
+		UriUriA tmp = {0};
+
+		if (php_uri_parser_rfc3986_add_base_url(&tmp, &uriparser_uris->uri, uriparser_base_uris, false) == FAILURE) {
+			goto failure;
+		}
+
+		uriMakeOwnerMmA(&tmp, mm);
+		uriFreeUriMembersMmA(&uriparser_uris->uri, mm);
+		uriparser_uris->uri = tmp;
+	}
+
+	return uriparser_uris;
+
+failure:
+	uriFreeUriMembersMmA(&uriparser_uris->uri, mm);
+	efree(uriparser_uris);
+	return NULL;
+}
+
 PHPAPI const php_uri_parser php_uri_parser_rfc3986 = {
 	.name = PHP_URI_PARSER_RFC3986,
 	.parse = php_uri_parser_rfc3986_parse,
diff --git a/ext/uri/uri_parser_rfc3986.h b/ext/uri/uri_parser_rfc3986.h
index a5fde707432..9a9f0b38a0c 100644
--- a/ext/uri/uri_parser_rfc3986.h
+++ b/ext/uri/uri_parser_rfc3986.h
@@ -29,4 +29,18 @@ zend_result php_uri_parser_rfc3986_userinfo_write(php_uri_parser_rfc3986_uris *u

 php_uri_parser_rfc3986_uris *php_uri_parser_rfc3986_parse_ex(const char *uri_str, size_t uri_str_len, const php_uri_parser_rfc3986_uris *uriparser_base_url, bool silent);

+ZEND_ATTRIBUTE_NONNULL zend_result php_uri_parser_rfc3986_validate_scheme(const zend_string *scheme);
+ZEND_ATTRIBUTE_NONNULL zend_result php_uri_parser_rfc3986_validate_userinfo(const zend_string *userinfo);
+ZEND_ATTRIBUTE_NONNULL zend_result php_uri_parser_rfc3986_validate_host(const zend_string *host);
+ZEND_ATTRIBUTE_NONNULL zend_result php_uri_parser_rfc3986_validate_port(zend_long port);
+ZEND_ATTRIBUTE_NONNULL zend_result php_uri_parser_rfc3986_validate_path(const zend_string *path);
+ZEND_ATTRIBUTE_NONNULL zend_result php_uri_parser_rfc3986_validate_query(const zend_string *query);
+ZEND_ATTRIBUTE_NONNULL zend_result php_uri_parser_rfc3986_validate_fragment(const zend_string *fragment);
+
+ZEND_ATTRIBUTE_NONNULL_ARGS(2,3,4,5,6,7,8) php_uri_parser_rfc3986_uris *php_uri_parser_rfc3986_build_from_zval(
+	const php_uri_parser_rfc3986_uris *uriparser_base_uris,
+	const zval *scheme, const zval *userinfo, const zval *host, const zval *port,
+	const zval *path, const zval *query, const zval *fragment
+);
+
 #endif