diff --git a/Zend/zend_hash.h b/Zend/zend_hash.h
index 5726c8a919..c462de2850 100644
--- a/Zend/zend_hash.h
+++ b/Zend/zend_hash.h
@@ -1087,6 +1087,10 @@ static zend_always_inline void *zend_hash_get_current_data_ptr_ex(HashTable *ht,
 	_ZEND_HASH_FOREACH_VAL(ht); \
 	_val = _z;
 
+#define ZEND_HASH_FOREACH_VAL_FROM(ht, _val, _from) \
+	ZEND_HASH_FOREACH_FROM(ht, 0, _from); \
+	_val = _z;
+
 #define ZEND_HASH_REVERSE_FOREACH_VAL(ht, _val) \
 	_ZEND_HASH_REVERSE_FOREACH_VAL(ht); \
 	_val = _z;
diff --git a/ext/standard/array.c b/ext/standard/array.c
index 46c2c882b8..171482113a 100644
--- a/ext/standard/array.c
+++ b/ext/standard/array.c
@@ -6394,6 +6394,75 @@ PHP_FUNCTION(array_map)
 }
 /* }}} */
 
+/* {{{ Groups consecutive elements from the array via the callback. */
+PHP_FUNCTION(array_group)
+{
+	zval *array;
+	zend_fcall_info fci;
+	zend_fcall_info_cache fci_cache = empty_fcall_info_cache;
+
+	zval args[2];
+	zval *prev_val;
+	zval *curr_val;
+	zval chunk;
+	zval retval;
+
+	ZEND_PARSE_PARAMETERS_START(2, 2)
+		Z_PARAM_ARRAY(array)
+		Z_PARAM_FUNC(fci, fci_cache)
+	ZEND_PARSE_PARAMETERS_END();
+
+	if (zend_hash_num_elements(Z_ARRVAL_P(array)) == 0) {
+		RETVAL_EMPTY_ARRAY();
+		return;
+	}
+
+	// The array is guaranteed to have at least one element.
+	prev_val = ZEND_HASH_ELEMENT(Z_ARRVAL_P(array), 0);
+
+	// Generate the initial group.
+	array_init(&chunk);
+	zend_hash_next_index_insert_new(Z_ARRVAL_P(&chunk), prev_val);
+
+	array_init(return_value);
+
+	fci.retval = &retval;
+	fci.param_count = 2;
+
+	ZEND_HASH_FOREACH_VAL_FROM(Z_ARRVAL_P(array), curr_val, 1) {
+		ZVAL_COPY(&args[0], prev_val);
+		ZVAL_COPY(&args[1], curr_val);
+		fci.params = args;
+
+		if (zend_call_function(&fci, &fci_cache) == SUCCESS && Z_TYPE(retval) != IS_UNDEF) {
+			int retval_true;
+
+			zval_ptr_dtor(&args[1]);
+			zval_ptr_dtor(&args[0]);
+
+			retval_true = zend_is_true(&retval);
+
+			zval_ptr_dtor(&retval);
+
+			// Perform grouping - add the current group and create a new one.
+			if (!retval_true) {
+				zend_hash_next_index_insert_new(Z_ARRVAL_P(return_value), &chunk);
+				array_init(&chunk);
+			}
+
+			zend_hash_next_index_insert_new(Z_ARRVAL_P(&chunk), curr_val);
+		} else {
+			zval_ptr_dtor(&args[1]);
+			zval_ptr_dtor(&args[0]);
+			RETURN_NULL();
+		}
+	} ZEND_HASH_FOREACH_END();
+
+	// Add the last group.
+	zend_hash_next_index_insert_new(Z_ARRVAL_P(return_value), &chunk);
+}
+/* }}} */
+
 /* {{{ Checks if the given key or index exists in the array */
 PHP_FUNCTION(array_key_exists)
 {
diff --git a/ext/standard/basic_functions.stub.php b/ext/standard/basic_functions.stub.php
index effb05ff9f..a6a8408dc9 100755
--- a/ext/standard/basic_functions.stub.php
+++ b/ext/standard/basic_functions.stub.php
@@ -1870,6 +1870,8 @@ function array_filter(array $array, ?callable $callback = null, int $mode = 0):
 
 function array_map(?callable $callback, array $array, array ...$arrays): array {}
 
+function array_group(array $array, callable $callback): array {}
+
 /**
  * @param string|int $key
  * @compile-time-eval
diff --git a/ext/standard/basic_functions_arginfo.h b/ext/standard/basic_functions_arginfo.h
index 5612ee2186..40600762f7 100644
--- a/ext/standard/basic_functions_arginfo.h
+++ b/ext/standard/basic_functions_arginfo.h
@@ -342,6 +342,11 @@ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_array_map, 0, 2, IS_ARRAY, 0)
 	ZEND_ARG_VARIADIC_TYPE_INFO(0, arrays, IS_ARRAY, 0)
 ZEND_END_ARG_INFO()
 
+ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_array_group, 0, 2, IS_ARRAY, 0)
+	ZEND_ARG_TYPE_INFO(0, array, IS_ARRAY, 0)
+	ZEND_ARG_TYPE_INFO(0, callback, IS_CALLABLE, 0)
+ZEND_END_ARG_INFO()
+
 ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_array_key_exists, 0, 2, _IS_BOOL, 0)
 	ZEND_ARG_INFO(0, key)
 	ZEND_ARG_TYPE_INFO(0, array, IS_ARRAY, 0)
@@ -2292,6 +2297,7 @@ ZEND_FUNCTION(array_product);
 ZEND_FUNCTION(array_reduce);
 ZEND_FUNCTION(array_filter);
 ZEND_FUNCTION(array_map);
+ZEND_FUNCTION(array_group);
 ZEND_FUNCTION(array_key_exists);
 ZEND_FUNCTION(array_chunk);
 ZEND_FUNCTION(array_combine);
@@ -2915,6 +2921,7 @@ static const zend_function_entry ext_functions[] = {
 	ZEND_FE(array_reduce, arginfo_array_reduce)
 	ZEND_FE(array_filter, arginfo_array_filter)
 	ZEND_FE(array_map, arginfo_array_map)
+	ZEND_FE(array_group, arginfo_array_group)
 	ZEND_SUPPORTS_COMPILE_TIME_EVAL_FE(array_key_exists, arginfo_array_key_exists)
 	ZEND_FALIAS(key_exists, array_key_exists, arginfo_key_exists)
 	ZEND_SUPPORTS_COMPILE_TIME_EVAL_FE(array_chunk, arginfo_array_chunk)
diff --git a/ext/standard/tests/array/array_group_basic.phpt b/ext/standard/tests/array/array_group_basic.phpt
new file mode 100644
index 0000000000..9db0e7afe3
--- /dev/null
+++ b/ext/standard/tests/array/array_group_basic.phpt
@@ -0,0 +1,130 @@
+--TEST--
+Test array_group() function : basic functionality
+--FILE--
+<?php
+echo "*** Testing array_group() : basic functionality ***\n";
+
+function less_than( $a, $b ) {
+	return $a < $b;
+}
+
+function equal( $a, $b ) {
+	return $a == $b;
+}
+
+function equal_obj( $a, $b ) {
+	return $a->name == $b->name;
+}
+
+$arr1 = array(1, 2, 3);
+
+echo "-- With an integer array for < --\n";
+var_dump( array_group($arr1, 'less_than') );
+
+echo "-- With an integer array for == --\n";
+var_dump( array_group($arr1, 'equal') );
+
+echo "-- With an empty integer array for == --\n";
+var_dump( array_group($arr1, 'equal') );
+
+echo "-- With a singleton integer array for == --\n";
+var_dump( array_group(array(1), 'equal') );
+
+$obj1 = (object)array('id'=>3,'name'=>'foo');
+$obj2 = (object)array('id'=>4,'name'=>'foo');
+$obj3 = (object)array('id'=>5,'name'=>'baz');
+
+echo "-- With a singleton integer array for == --\n";
+var_dump( array_group(array($obj1, $obj2, $obj3), 'equal_obj') );
+
+echo "Done";
+?>
+--EXPECT--
+*** Testing array_group() : basic functionality ***
+-- With an integer array for < --
+array(1) {
+  [0]=>
+  array(3) {
+    [0]=>
+    int(1)
+    [1]=>
+    int(2)
+    [2]=>
+    int(3)
+  }
+}
+-- With an integer array for == --
+array(3) {
+  [0]=>
+  array(1) {
+    [0]=>
+    int(1)
+  }
+  [1]=>
+  array(1) {
+    [0]=>
+    int(2)
+  }
+  [2]=>
+  array(1) {
+    [0]=>
+    int(3)
+  }
+}
+-- With an empty integer array for == --
+array(3) {
+  [0]=>
+  array(1) {
+    [0]=>
+    int(1)
+  }
+  [1]=>
+  array(1) {
+    [0]=>
+    int(2)
+  }
+  [2]=>
+  array(1) {
+    [0]=>
+    int(3)
+  }
+}
+-- With a singleton integer array for == --
+array(1) {
+  [0]=>
+  array(1) {
+    [0]=>
+    int(1)
+  }
+}
+-- With a singleton integer array for == --
+array(2) {
+  [0]=>
+  array(2) {
+    [0]=>
+    object(stdClass)#1 (2) {
+      ["id"]=>
+      int(3)
+      ["name"]=>
+      string(3) "foo"
+    }
+    [1]=>
+    object(stdClass)#2 (2) {
+      ["id"]=>
+      int(4)
+      ["name"]=>
+      string(3) "foo"
+    }
+  }
+  [1]=>
+  array(1) {
+    [0]=>
+    object(stdClass)#3 (2) {
+      ["id"]=>
+      int(5)
+      ["name"]=>
+      string(3) "baz"
+    }
+  }
+}
+Done
