A full regex solution is not possible for this specific case.
Your solution adapted to match paired tags (in the common sense):
$pattern = '~\[foo]((?>[^[]++|\[(?!/?foo]))*)\[/foo]~';
$result = $text;
do {
$result = preg_replace($pattern, '[bar]$1[/bar]', $result, -1, $count);
} while ($count);
Another way that parses the string only once:
$arr = preg_split('~(\[/?foo])~', $text, -1, PREG_SPLIT_DELIM_CAPTURE|PREG_SPLIT_NO_EMPTY);
$stack = array();
foreach ($arr as $key=>$item) {
if ($item == '[foo]') $stack[] = $key;
else if ($item == '[/foo]' && !empty($stack)) {
$arr[array_pop($stack)] = '[bar]';
$arr[$key] = '[/bar]';
}
}
$result = implode($arr);
the performance of this second script is independant of the depth.
To answer the title question, yes it is possible to find overlapping matches with a single regex, however, you can't perform a replacement with this kind of pattern, example:
$pattern = '~(?=(\[foo]((?>[^[]++|\[(?!/?foo)|(?1))*)\[/foo]))~';
preg_match_all($pattern, $text, $matches);
The trick is to use a lookahead and a capturing group. Note that the whole match is always an empty string, this is the reason why you can't use this pattern with preg_replace.