I'll post the self-answer with my PHP 8 knowledge in 2022.
It correctly accepts negative length/offset and string offset.
function array_splice_assoc(array &$input, int|string $key, ?int $length = null, $replacement = [], bool $use_int_key_as_offset = true): array
{
// Normalize key/offset
$offset = match (true) {
is_string($key) || !$use_int_key_as_offset => array_flip(array_keys($input))[$key] ?? throw new OutOfBoundsException(),
$key < 0 => count($input) + $key,
default => $key,
};
// Normalize length
$length = match (true) {
$length === null => count($input) - $offset,
$length < 0 => count($input) + $length - $offset,
default => $length,
};
// Manipulate each part
$before = array_slice($input, 0, $offset, true);
$removed = array_slice($input, $offset, $length, true);
$after = array_slice($input, $offset + $length, null, true);
// Merge parts, allowing the latter overrides the former
$input = array_replace($before, (array)$replacement, $after);
return $removed;
}
Examples:
$array = ['a' => 'A', 'b' => 'B', 3 => 'C', 4 => 'D'];
$original = $array;
$removed = array_splice_assoc($original, 1, 1, [5 => 'E']);
echo json_encode(compact('original', 'removed')) . PHP_EOL;
/*
{"original":{"a":"A","5":"E","3":"C","4":"D"},"removed":{"b":"B"}}
*/
$original = $array;
$removed = array_splice_assoc($original, 2, replacement: [5 => 'E']);
echo json_encode(compact('original', 'removed')) . PHP_EOL;
/*
{"original":{"a":"A","b":"B","5":"E"},"removed":{"3":"C","4":"D"}}
*/
$original = $array;
$removed = array_splice_assoc($original, -3, 1, [5 => 'E']);
echo json_encode(compact('original', 'removed')) . PHP_EOL;
/*
{"original":{"a":"A","5":"E","3":"C","4":"D"},"removed":{"b":"B"}}
*/
$original = $array;
$removed = array_splice_assoc($original, -3, -1, [5 => 'E']);
echo json_encode(compact('original', 'removed')) . PHP_EOL;
/*
{"original":{"a":"A","5":"E","4":"D"},"removed":{"b":"B","3":"C"}}
*/
$original = $array;
$removed = array_splice_assoc($original, 'b', 2, [5 => 'E']);
echo json_encode(compact('original', 'removed')) . PHP_EOL;
/*
{"original":{"a":"A","5":"E","4":"D"},"removed":{"b":"B","3":"C"}}
*/
$original = $array;
$removed = array_splice_assoc($original, 3, 1, [5 => 'E']);
echo json_encode(compact('original', 'removed')) . PHP_EOL;
/*
{"original":{"a":"A","b":"B","3":"C","5":"E"},"removed":{"4":"D"}}
*/
$original = $array;
$removed = array_splice_assoc($original, 3, 1, [5 => 'E'], false);
echo json_encode(compact('original', 'removed')) . PHP_EOL;
/*
{"original":{"a":"A","b":"B","5":"E","4":"D"},"removed":{"3":"C"}}
*/