Если ваш вариант с регулярным выражением не поддерживает не жадное сопоставление, это сделает следующее, но так как я рекомендую @ BaileyP ответ .
\{foreach [^}]*\}((?:.(?!\{/foreach\}))*[^{]?)
В зависимости от вашего предпочтения в регулярном выражении группы с отрицательным прогнозом нулевой ширины и группы без захвата выглядят немного по-разному.
Вот компоненты:
\{foreach [^}]*\} // pretty much self-explanatory
( // match group one starts (that's what you are looking for)
(?: // non-capturing group starts
. // anything...
(?!\{/foreach\}) // ... that is not followed by "{/foreach}"
)* // non-capturing group ends, repeat as often as possible
[^{]? // match the last character, unless it is "{"
) // match group one ends, done