diff --git a/Zend/tests/closure_composition/auto-promote-closure.phpt b/Zend/tests/closure_composition/auto-promote-closure.phpt
new file mode 100644
index 0000000000000..0d6fee18c4c97
--- /dev/null
+++ b/Zend/tests/closure_composition/auto-promote-closure.phpt
@@ -0,0 +1,9 @@
+--TEST--
+Closures get auto promoted to a Composed Closure.
+--FILE--
+<?php
+
+$cb = strtolower(...) + str_rot13(...) + (fn($x) => "<$x>");
+var_dump($cb("Hello World"));
+--EXPECT--
+string(13) "<uryyb jbeyq>"
diff --git a/Zend/tests/closure_composition/compose-operator.phpt b/Zend/tests/closure_composition/compose-operator.phpt
new file mode 100644
index 0000000000000..63d606ccea7b9
--- /dev/null
+++ b/Zend/tests/closure_composition/compose-operator.phpt
@@ -0,0 +1,12 @@
+--TEST--
+Composed callable using composition.
+--FILE--
+<?php
+
+$cb1 = new \ComposedCallable([strtolower(...)]);
+$cb2 = $cb1 + strrev(...);
+var_dump($cb1("Hello World"));
+var_dump($cb2("Hello World"));
+--EXPECT--
+string(11) "hello world"
+string(11) "dlrow olleh"
diff --git a/Zend/tests/closure_composition/direct-methods.phpt b/Zend/tests/closure_composition/direct-methods.phpt
new file mode 100644
index 0000000000000..e1682af71f268
--- /dev/null
+++ b/Zend/tests/closure_composition/direct-methods.phpt
@@ -0,0 +1,18 @@
+--TEST--
+Composed callable using direct object methods.
+--FILE--
+<?php
+
+$cb = new \ComposedCallable(strtolower(...));
+var_dump($cb("Hello"));
+$cb->prepend(fn($x) => "$x World");
+var_dump($cb("Hello"));
+$cb->append('strrev');
+var_dump($cb("Hello"));
+$cb->insert(ucfirst(...), 2);
+var_dump($cb("Hello"));
+--EXPECT--
+string(5) "hello"
+string(11) "hello world"
+string(11) "dlrow olleh"
+string(11) "dlrow olleH"
diff --git a/Zend/tests/closure_composition/invokable.phpt b/Zend/tests/closure_composition/invokable.phpt
new file mode 100644
index 0000000000000..453a2384238ca
--- /dev/null
+++ b/Zend/tests/closure_composition/invokable.phpt
@@ -0,0 +1,27 @@
+--TEST--
+Invokable objects may be composed
+--FILE--
+<?php
+
+class Times {
+    public function __construct(private int $x) {}
+
+    public function __invoke(int $y): int
+    {
+        return $this->x * $y;
+    }
+}
+
+function double(int $x): int {
+    return $x * 2;
+}
+
+$cb1 = double(...) + new Times(3);
+var_dump($cb1(4));
+
+$cb2 = new Times(3) + double(...);
+var_dump($cb2(4));
+
+--EXPECT--
+int(24)
+int(24)
diff --git a/Zend/zend_closures.c b/Zend/zend_closures.c
index 5777e1a34a2b8..6a4a2ca29ddac 100644
--- a/Zend/zend_closures.c
+++ b/Zend/zend_closures.c
@@ -21,6 +21,7 @@
 #include "zend.h"
 #include "zend_API.h"
 #include "zend_closures.h"
+#include "zend_composed_callable.h"
 #include "zend_exceptions.h"
 #include "zend_interfaces.h"
 #include "zend_objects.h"
@@ -705,6 +706,14 @@ ZEND_COLD ZEND_METHOD(Closure, __construct)
 }
 /* }}} */
 
+static zend_result zend_closure_do_operation(uint8_t opcode, zval *result, zval *op1, zval *op2) {
+	if (opcode != ZEND_ADD) {
+		return FAILURE;
+	}
+
+	return zend_composed_callable_new_from_pair(result, op1, op2);
+}
+
 void zend_register_closure_ce(void) /* {{{ */
 {
 	zend_ce_closure = register_class_Closure();
@@ -720,6 +729,7 @@ void zend_register_closure_ce(void) /* {{{ */
 	closure_handlers.get_debug_info = zend_closure_get_debug_info;
 	closure_handlers.get_closure = zend_closure_get_closure;
 	closure_handlers.get_gc = zend_closure_get_gc;
+	closure_handlers.do_operation = zend_closure_do_operation;
 }
 /* }}} */
 
diff --git a/Zend/zend_composed_callable.c b/Zend/zend_composed_callable.c
new file mode 100644
index 0000000000000..46da5f82e7a4a
--- /dev/null
+++ b/Zend/zend_composed_callable.c
@@ -0,0 +1,373 @@
+/*
+   +----------------------------------------------------------------------+
+   | Zend Engine                                                          |
+   +----------------------------------------------------------------------+
+   | Copyright (c) Zend Technologies Ltd. (http://www.zend.com)           |
+   +----------------------------------------------------------------------+
+   | This source file is subject to version 2.00 of the Zend license,     |
+   | that is bundled with this package in the file LICENSE, and is        |
+   | available through the world-wide-web at the following url:           |
+   | http://www.zend.com/license/2_00.txt.                                |
+   | If you did not receive a copy of the Zend license and are unable to  |
+   | obtain it through the world-wide-web, please send a note to          |
+   | license@zend.com so we can mail you a copy immediately.              |
+   +----------------------------------------------------------------------+
+   | Authors: Sara Golemon <pollita@php.net>                              |
+   +----------------------------------------------------------------------+
+*/
+
+#include "zend.h"
+#include "zend_API.h"
+#include "zend_composed_callable.h"
+#include "zend_exceptions.h"
+
+#define ZEND_COMPOSED_CALLABLE_INITIAL_CAPACITY 8
+
+ZEND_API zend_class_entry *zend_ce_composed_callable;
+static zend_object_handlers zend_composed_callable_handlers;
+
+typedef struct {
+	HashTable callables;
+	zend_object std;
+} zend_composed_callable;
+
+static zend_fcall_info_cache* zend_composed_callable_fcc_from_zval(zval* pzv) {
+	ZEND_ASSERT(Z_TYPE_P(pzv) == IS_PTR);
+	return Z_PTR_P(pzv);
+}
+
+static zend_object* zend_composed_callable_to_zend_object(zend_composed_callable* objval) {
+	return ((zend_object*)(objval + 1)) - 1;
+}
+
+static zend_composed_callable* zend_composed_callable_from_zend_object(zend_object* zobj) {
+	return ((zend_composed_callable*)(zobj + 1)) - 1;
+}
+
+static zend_result zend_composed_callable_append1(HashTable *callables, zval *callable) {
+	zend_fcall_info_cache *fcc = ecalloc(1, sizeof(zend_fcall_info_cache));
+	char *error = NULL;
+	zval ptr;
+
+	if (!zend_is_callable_ex(callable, NULL, 0, NULL, fcc, &error)) {
+		if (error) {
+			zend_throw_exception_ex(zend_ce_value_error, 0, "Argument not valid callback: %s", error);
+			efree(error);
+		} else {
+			zend_throw_exception(zend_ce_value_error, "Unable to initialize callback", 0);
+		}
+		efree(fcc);
+		return FAILURE;
+	}
+
+	ZVAL_PTR(&ptr, fcc);
+	zend_fcc_addref(fcc);
+	zend_hash_next_index_insert(callables, &ptr);
+
+	return SUCCESS;
+}
+
+static zend_result zend_composed_callable_splice(HashTable *dst, HashTable *src, zend_long pos, zval *insert);
+static zend_result zend_composed_callable_append(HashTable *callables, zval *callable) {
+	if (Z_TYPE_P(callable) == IS_ARRAY) {
+		zval *fn;
+
+		ZEND_HASH_FOREACH_VAL(Z_ARR_P(callable), fn) {
+			if (zend_composed_callable_append1(callables, fn) == FAILURE) {
+				return FAILURE;
+			}
+		} ZEND_HASH_FOREACH_END();
+
+		return SUCCESS;
+	} else if ((Z_TYPE_P(callable) == IS_OBJECT) && instanceof_function(Z_OBJCE_P(callable), zend_ce_composed_callable)) {
+		HashTable *src_callables = &(zend_composed_callable_from_zend_object(Z_OBJ_P(callable))->callables);
+		return zend_composed_callable_splice(callables, src_callables, 0, NULL);
+	} else {
+		return zend_composed_callable_append1(callables, callable);
+	}
+}
+
+ZEND_METHOD(ComposedCallable, __construct) {
+	HashTable *callables = &(zend_composed_callable_from_zend_object(Z_OBJ_P(getThis()))->callables);
+	zval *callable;
+
+	if (zend_parse_parameters_throw(ZEND_NUM_ARGS(), "z", &callable) == FAILURE) {
+		RETURN_THROWS();
+	}
+
+	if (zend_composed_callable_append(callables, callable) == FAILURE) {
+		RETURN_THROWS();
+	}
+}
+
+ZEND_METHOD(ComposedCallable, __invoke) {
+	HashTable *callables = &(zend_composed_callable_from_zend_object(Z_OBJ_P(getThis()))->callables);
+	zval *argv, *callable;
+	uint32_t argc = 0, i;
+
+	if (zend_hash_num_elements(callables) < 1) {
+		zend_throw_exception(zend_ce_value_error, "Attempt to invoke an empty ComposedCallable", 0);
+		RETURN_THROWS();
+	}
+
+	if (zend_parse_parameters_throw(ZEND_NUM_ARGS(), "*", &argv, &argc) == FAILURE) {
+		RETURN_THROWS();
+	}
+
+	/* Prevent destruction of initial args during intermediate forwarding. */
+	for (i = 0; i < argc; ++i) {
+		Z_TRY_ADDREF(argv[i]);
+	}
+
+	ZEND_HASH_FOREACH_VAL(callables, callable) {
+		zend_fcall_info_cache *fcc = zend_composed_callable_fcc_from_zval(callable);
+		zval retval;
+
+		zend_call_known_fcc(fcc, &retval, argc, argv, NULL);
+
+		/* Clean up the prior call vars. */
+		for (i = 0; i < argc; ++i) {
+			zval_ptr_dtor(&(argv[i]));
+		}
+
+		/* Forward retval to next invocation. */
+		argv[0] = retval;
+		argc = 1;
+	} ZEND_HASH_FOREACH_END();
+
+	/* The check at the top of this method ensures we call at least one callable,
+	 * and every callable has precisely one return value (even if it's NULL).
+	 */
+	ZEND_ASSERT(argc == 1);
+	RETURN_ZVAL(&(argv[0]), 1, 0);
+}
+
+static zend_result zend_composed_callable_splice(HashTable *dst, HashTable *src, zend_long pos, zval *insert) {
+	zval *src_callable;
+	ZEND_HASH_FOREACH_VAL(src, src_callable) {
+		if (insert && pos-- == 0) {
+			if (zend_composed_callable_append(dst, insert) == FAILURE) {
+				return FAILURE;
+			}
+			insert = NULL;
+			/* fallthrough to continue splice */
+		}
+
+		{
+			zend_fcall_info_cache *src_fcc = zend_composed_callable_fcc_from_zval(src_callable);
+			zend_fcall_info_cache *dst_fcc = ecalloc(1, sizeof(zend_fcall_info_cache));
+			zval dst_callable;
+			zend_fcc_dup(dst_fcc, src_fcc);
+			ZVAL_PTR(&dst_callable, dst_fcc);
+			zend_hash_next_index_insert(dst, &dst_callable);
+		}
+	} ZEND_HASH_FOREACH_END();
+
+	/* append splice */
+	if (insert && (zend_composed_callable_append(dst, insert) == FAILURE)) {
+		return FAILURE;
+	}
+
+	return SUCCESS;
+}
+
+static void zend_composed_callable_fcc_dtor(zval *ptr) {
+	zend_fcall_info_cache *fcc = zend_composed_callable_fcc_from_zval(ptr);
+	zend_fcc_dtor(fcc);
+	efree(fcc);
+}
+
+static zend_result zend_composed_callable_do_insert(HashTable *callables, zend_long pos, zval *callable) {
+	zend_long num_callables = zend_hash_num_elements(callables);
+
+	if (pos < 0) {
+		/* For "-2" meaning "right before the last one" kind of semantics */
+		pos += num_callables;
+	}
+
+	if (pos < 0) {
+		pos = 0;
+		/* Maybe error? */
+	}
+
+	if (pos > num_callables) {
+		pos = num_callables;
+		/* Maybe error? */
+	}
+
+	if (pos == num_callables) {
+		/* Quick method. This is just append() with more steps. */
+		return zend_composed_callable_append(callables, callable);
+	}
+
+	/* Slow path, rebuild the hashtable :( */
+	HashTable newtable;
+	zend_hash_init(&newtable, ZEND_COMPOSED_CALLABLE_INITIAL_CAPACITY, NULL, zend_composed_callable_fcc_dtor, 0);
+	if (zend_composed_callable_splice(&newtable, callables, pos, callable) == FAILURE) {
+		zend_hash_destroy(&newtable);
+		return FAILURE;
+	}
+
+	zend_hash_destroy(callables);
+	*callables = newtable;
+	return SUCCESS;
+}
+
+ZEND_METHOD(ComposedCallable, insert) {
+	HashTable *callables = &(zend_composed_callable_from_zend_object(Z_OBJ_P(getThis()))->callables);
+	zval *callable;
+	zend_long pos;
+
+	if (zend_parse_parameters_throw(ZEND_NUM_ARGS(), "zl", &callable, &pos) == FAILURE) {
+		RETURN_THROWS();
+	}
+
+	if (zend_composed_callable_do_insert(callables, pos, callable) == FAILURE) {
+		RETURN_THROWS();
+	}
+
+	RETURN_ZVAL(getThis(), 1, 0);
+}
+
+ZEND_METHOD(ComposedCallable, prepend) {
+	HashTable *callables = &(zend_composed_callable_from_zend_object(Z_OBJ_P(getThis()))->callables);
+	zval *callable;
+
+	if (zend_parse_parameters_throw(ZEND_NUM_ARGS(), "z", &callable) == FAILURE) {
+		RETURN_THROWS();
+	}
+
+	if (zend_composed_callable_do_insert(callables, 0, callable) == FAILURE) {
+		RETURN_THROWS();
+	}
+
+	RETURN_ZVAL(getThis(), 1, 0);
+}
+
+ZEND_METHOD(ComposedCallable, append) {
+    HashTable *callables = &(zend_composed_callable_from_zend_object(Z_OBJ_P(getThis()))->callables);
+    zval *callable;
+
+    if (zend_parse_parameters_throw(ZEND_NUM_ARGS(), "z", &callable) == FAILURE) {
+        RETURN_THROWS();
+    }
+
+    if (zend_composed_callable_append(callables, callable) == FAILURE) {
+        RETURN_THROWS();
+    }
+
+	RETURN_ZVAL(getThis(), 1, 0);
+}
+
+ZEND_API zend_result zend_composed_callable_new_from_pair(zval *result, zval *callable1, zval *callable2) {
+	zend_object *ret = zend_ce_composed_callable->create_object(zend_ce_composed_callable);
+	HashTable *callables = &(zend_composed_callable_from_zend_object(ret)->callables);
+
+	if ((zend_composed_callable_append(callables, callable1) == FAILURE) ||
+		(zend_composed_callable_append(callables, callable2) == FAILURE)) {
+		return FAILURE;
+	}
+
+	ZVAL_OBJ(result, ret);
+	return SUCCESS;
+}
+
+static zend_result zend_composed_callable_do_operation(uint8_t opcode, zval *result, zval *op1, zval *op2) {
+	bool append;
+
+	if (opcode != ZEND_ADD) {
+		return FAILURE;
+	}
+
+	if ((Z_TYPE_P(op1) == IS_OBJECT) && instanceof_function(Z_OBJCE_P(op1), zend_ce_composed_callable)) {
+		/* composed + callableOrComposed
+		 * Clone op1's invocations into new result,
+		 * then do standard append on op2, whatever it is.
+		 */
+		append = true;
+	} else {
+		ZEND_ASSERT((Z_TYPE_P(op2) == IS_OBJECT) && instanceof_function(Z_OBJCE_P(op2), zend_ce_composed_callable));
+		/* callable + composed
+		 * Start with just the initial callable, then clone in rhs' entries.
+		 */
+		append = false;
+	}
+
+	{
+		zend_object *composed_obj = append ? Z_OBJ_P(op1) : Z_OBJ_P(op2);
+		HashTable *src_callables = &(zend_composed_callable_from_zend_object(composed_obj)->callables);
+		uint32_t pos = append ? zend_hash_num_elements(src_callables) : 0;
+
+		zend_object *ret = composed_obj->ce->create_object(composed_obj->ce);
+		HashTable *dst_callables = &(zend_composed_callable_from_zend_object(ret)->callables);
+
+		zval *insert_obj = append ? op2 : op1;
+
+		ZVAL_OBJ(result, ret);
+		if (zend_composed_callable_splice(dst_callables, src_callables, pos, insert_obj) == FAILURE) {
+			zval_ptr_dtor(result);
+			ZVAL_UNDEF(result);
+			return FAILURE;
+		}
+
+		return SUCCESS;
+	}
+}
+
+ZEND_METHOD(ComposedCallable, __debugInfo) {
+	HashTable *callables = &(zend_composed_callable_from_zend_object(Z_OBJ_P(getThis()))->callables);
+
+	if (zend_parse_parameters_none_throw() == FAILURE) {
+		RETURN_THROWS();
+	}
+
+	array_init(return_value);
+	{
+		zval callables_names, *callable;
+		array_init(&callables_names);
+		ZEND_HASH_FOREACH_VAL(callables, callable) {
+			zend_string *name = zend_composed_callable_fcc_from_zval(callable)->function_handler->common.function_name;
+			add_next_index_str(&callables_names, name);
+		} ZEND_HASH_FOREACH_END();
+		add_assoc_zval(return_value, "callables", &callables_names);
+	}
+}
+
+static zend_object *zend_composed_callable_create(zend_class_entry *ce) {
+	zend_composed_callable *objval = ecalloc(1, sizeof(zend_composed_callable) + zend_object_properties_size(ce));
+	zend_object* ret = zend_composed_callable_to_zend_object(objval);
+	zend_object_std_init(ret, ce);
+	zend_hash_init(&objval->callables, ZEND_COMPOSED_CALLABLE_INITIAL_CAPACITY, NULL, zend_composed_callable_fcc_dtor, 0);
+	ret->handlers = &zend_composed_callable_handlers;
+	return ret;
+}
+
+static zend_object* zend_composed_callable_clone(zend_object* zsrc) {
+    zend_object *zdst = zsrc->ce->create_object(zsrc->ce);
+    zend_objects_clone_members(zdst, zsrc);
+
+    zend_composed_callable *src = zend_composed_callable_from_zend_object(zsrc);
+    zend_composed_callable *dst = zend_composed_callable_from_zend_object(zdst);
+
+	zend_composed_callable_splice(&dst->callables, &src->callables, 0, NULL);
+	return zdst;
+}
+
+static void zend_composed_callable_free(zend_object *zobj) {
+	zend_composed_callable *obj = zend_composed_callable_from_zend_object(zobj);
+	zend_object_std_dtor(zobj);
+	zend_hash_destroy(&obj->callables);
+	efree(obj);
+}
+
+void zend_register_composed_callable(zend_class_entry *ce) {
+	zend_ce_composed_callable = ce;
+	ce->create_object = zend_composed_callable_create;
+
+	memcpy(&zend_composed_callable_handlers, zend_get_std_object_handlers(), sizeof(zend_object_handlers));
+	zend_composed_callable_handlers.offset = XtOffsetOf(zend_composed_callable, std);
+	zend_composed_callable_handlers.clone_obj = zend_composed_callable_clone;
+	zend_composed_callable_handlers.free_obj = zend_composed_callable_free;
+
+	zend_composed_callable_handlers.do_operation = zend_composed_callable_do_operation;
+}
diff --git a/Zend/zend_composed_callable.h b/Zend/zend_composed_callable.h
new file mode 100644
index 0000000000000..8e36cb26eebd6
--- /dev/null
+++ b/Zend/zend_composed_callable.h
@@ -0,0 +1,35 @@
+/*
+   +----------------------------------------------------------------------+
+   | Zend Engine                                                          |
+   +----------------------------------------------------------------------+
+   | Copyright (c) Zend Technologies Ltd. (http://www.zend.com)           |
+   +----------------------------------------------------------------------+
+   | This source file is subject to version 2.00 of the Zend license,     |
+   | that is bundled with this package in the file LICENSE, and is        |
+   | available through the world-wide-web at the following url:           |
+   | http://www.zend.com/license/2_00.txt.                                |
+   | If you did not receive a copy of the Zend license and are unable to  |
+   | obtain it through the world-wide-web, please send a note to          |
+   | license@zend.com so we can mail you a copy immediately.              |
+   +----------------------------------------------------------------------+
+   | Authors: Sara Golemon <pollita@php.net>                              |
+   +----------------------------------------------------------------------+
+*/
+
+#ifndef ZEND_COMPOSED_CALLABLE_H
+#define ZEND_COMPOSED_CALLABLE_H
+
+#include "zend.h"
+#include "zend_API.h"
+
+BEGIN_EXTERN_C()
+
+extern ZEND_API zend_class_entry *zend_ce_composed_callable;
+
+ZEND_API zend_result zend_composed_callable_new_from_pair(zval *result, zval *callable1, zval *callable2);
+
+void zend_register_composed_callable(zend_class_entry *ce);
+
+END_EXTERN_C()
+
+#endif /* ZEND_COMPOSED_CALLABLE_H */
diff --git a/Zend/zend_interfaces.c b/Zend/zend_interfaces.c
index ce9cc00fdfb95..d1bf51f1bb93b 100644
--- a/Zend/zend_interfaces.c
+++ b/Zend/zend_interfaces.c
@@ -18,6 +18,7 @@
 
 #include "zend.h"
 #include "zend_API.h"
+#include "zend_composed_callable.h"
 #include "zend_interfaces.h"
 #include "zend_exceptions.h"
 #include "zend_interfaces_arginfo.h"
@@ -676,5 +677,7 @@ ZEND_API void zend_register_interfaces(void)
 		sizeof(zend_object_handlers));
 	zend_internal_iterator_handlers.clone_obj = NULL;
 	zend_internal_iterator_handlers.free_obj = zend_internal_iterator_free;
+
+    zend_register_composed_callable(register_class_ComposedCallable());
 }
 /* }}} */
diff --git a/Zend/zend_interfaces.stub.php b/Zend/zend_interfaces.stub.php
index 2247205e46973..a3bd39ef41db5 100644
--- a/Zend/zend_interfaces.stub.php
+++ b/Zend/zend_interfaces.stub.php
@@ -83,3 +83,12 @@ public function valid(): bool {}
 
     public function rewind(): void {}
 }
+
+final class ComposedCallable {
+    public function __construct(array|callable|\ComposedCallable $callable) {}
+    public function __invoke(mixed ...$args): mixed {}
+    public function __debugInfo(): array {}
+    public function append(callable|\ComposedCallable $callable): \ComposedCallable {}
+    public function insert(callable|\ComposedCallable $callable, int $idx): \ComposedCallable {}
+    public function prepend(callable|\ComposedCallable $callable): \ComposedCallable {}
+}
diff --git a/Zend/zend_interfaces_arginfo.h b/Zend/zend_interfaces_arginfo.h
index 8a90166b2d800..70a5445628c66 100644
--- a/Zend/zend_interfaces_arginfo.h
+++ b/Zend/zend_interfaces_arginfo.h
@@ -1,5 +1,5 @@
 /* This is a generated file, edit the .stub.php file instead.
- * Stub hash: a9c915c11e5989d8c7cf2d704ada09ca765670c3 */
+ * Stub hash: 2dcce186cc58725fdb4127d15b29cd9bcb0968af */
 
 ZEND_BEGIN_ARG_WITH_TENTATIVE_RETURN_OBJ_INFO_EX(arginfo_class_IteratorAggregate_getIterator, 0, 0, Traversable, 0)
 ZEND_END_ARG_INFO()
@@ -62,12 +62,40 @@ ZEND_END_ARG_INFO()
 
 #define arginfo_class_InternalIterator_rewind arginfo_class_InternalIterator_next
 
+ZEND_BEGIN_ARG_INFO_EX(arginfo_class_ComposedCallable___construct, 0, 0, 1)
+	ZEND_ARG_OBJ_TYPE_MASK(0, callable, ComposedCallable, MAY_BE_ARRAY|MAY_BE_CALLABLE, NULL)
+ZEND_END_ARG_INFO()
+
+ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_ComposedCallable___invoke, 0, 0, IS_MIXED, 0)
+	ZEND_ARG_VARIADIC_TYPE_INFO(0, args, IS_MIXED, 0)
+ZEND_END_ARG_INFO()
+
+ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_ComposedCallable___debugInfo, 0, 0, IS_ARRAY, 0)
+ZEND_END_ARG_INFO()
+
+ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_ComposedCallable_append, 0, 1, ComposedCallable, 0)
+	ZEND_ARG_OBJ_TYPE_MASK(0, callable, ComposedCallable, MAY_BE_CALLABLE, NULL)
+ZEND_END_ARG_INFO()
+
+ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_ComposedCallable_insert, 0, 2, ComposedCallable, 0)
+	ZEND_ARG_OBJ_TYPE_MASK(0, callable, ComposedCallable, MAY_BE_CALLABLE, NULL)
+	ZEND_ARG_TYPE_INFO(0, idx, IS_LONG, 0)
+ZEND_END_ARG_INFO()
+
+#define arginfo_class_ComposedCallable_prepend arginfo_class_ComposedCallable_append
+
 ZEND_METHOD(InternalIterator, __construct);
 ZEND_METHOD(InternalIterator, current);
 ZEND_METHOD(InternalIterator, key);
 ZEND_METHOD(InternalIterator, next);
 ZEND_METHOD(InternalIterator, valid);
 ZEND_METHOD(InternalIterator, rewind);
+ZEND_METHOD(ComposedCallable, __construct);
+ZEND_METHOD(ComposedCallable, __invoke);
+ZEND_METHOD(ComposedCallable, __debugInfo);
+ZEND_METHOD(ComposedCallable, append);
+ZEND_METHOD(ComposedCallable, insert);
+ZEND_METHOD(ComposedCallable, prepend);
 
 static const zend_function_entry class_IteratorAggregate_methods[] = {
 	ZEND_RAW_FENTRY("getIterator", NULL, arginfo_class_IteratorAggregate_getIterator, ZEND_ACC_PUBLIC|ZEND_ACC_ABSTRACT, NULL, NULL)
@@ -117,6 +145,16 @@ static const zend_function_entry class_InternalIterator_methods[] = {
 	ZEND_FE_END
 };
 
+static const zend_function_entry class_ComposedCallable_methods[] = {
+	ZEND_ME(ComposedCallable, __construct, arginfo_class_ComposedCallable___construct, ZEND_ACC_PUBLIC)
+	ZEND_ME(ComposedCallable, __invoke, arginfo_class_ComposedCallable___invoke, ZEND_ACC_PUBLIC)
+	ZEND_ME(ComposedCallable, __debugInfo, arginfo_class_ComposedCallable___debugInfo, ZEND_ACC_PUBLIC)
+	ZEND_ME(ComposedCallable, append, arginfo_class_ComposedCallable_append, ZEND_ACC_PUBLIC)
+	ZEND_ME(ComposedCallable, insert, arginfo_class_ComposedCallable_insert, ZEND_ACC_PUBLIC)
+	ZEND_ME(ComposedCallable, prepend, arginfo_class_ComposedCallable_prepend, ZEND_ACC_PUBLIC)
+	ZEND_FE_END
+};
+
 static zend_class_entry *register_class_Traversable(void)
 {
 	zend_class_entry ce, *class_entry;
@@ -199,3 +237,13 @@ static zend_class_entry *register_class_InternalIterator(zend_class_entry *class
 
 	return class_entry;
 }
+
+static zend_class_entry *register_class_ComposedCallable(void)
+{
+	zend_class_entry ce, *class_entry;
+
+	INIT_CLASS_ENTRY(ce, "ComposedCallable", class_ComposedCallable_methods);
+	class_entry = zend_register_internal_class_with_flags(&ce, NULL, ZEND_ACC_FINAL);
+
+	return class_entry;
+}
diff --git a/configure.ac b/configure.ac
index e4bd8162a2ebc..37e1a93f1ec4d 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1737,6 +1737,7 @@ PHP_ADD_SOURCES([Zend], m4_normalize([
     zend_call_stack.c
     zend_closures.c
     zend_compile.c
+    zend_composed_callable.c
     zend_constants.c
     zend_cpuinfo.c
     zend_default_classes.c