Template parsing function
Escaping the RegExp

You should never do manual escaping, if an appropriate function exists. For RegExp, this is preg_quote. Another risk is that you'r escaping before splitting, so you might destroy an escaping sequence. Better do

function substitute($template, $values, $delimiter = '{??}')
{
    $half    = (int) ( (strlen($delimiter) / 2) );
    $begin   = preg_quote(substr($delimiter, 0, $half), '/');
    $end     = preg_quote(substr($delimiter, $half), '/');
    $pattern = '/' . $begin . '(\w+)' . $end . '/';


Escaping the Values

The user provided values maust be escaped according to the context, where they are used. In this case it is HTML, so you must suppress unwanted tags, especially scripting tags. You can use strip_tags() or htmlspecialchars() for that purpose. I prefer the latter, because the user's value is not changed.

    return preg_replace_callback(
        $pattern,
        function ($matches) use ($values) {
            if (isset($values[$matches[1]])) {
                return htmlspecialchars($values[$matches[1]]);
            }
            return $matches[0];
        },
        $template);
    }
}


Conclusion

With these two small changes you have done everything to make that function save.