Если имя пользователя не присутствует каждый раз, вы можете сделать эту часть необязательной и использовать 2 группы захвата и preg_replace_callback .
В обратном вызове проверьте, если значение для группы 1 (имя пользователя) не пусто и используйте групповые значения для сборки значений ссылки.
(?<!\S)(?:(@[^\s@]+)(?!\S)[^h]*(?:h(?!ttp)[^h]*)*+)?\K((?:https?|ftp|file)://\S+)
Объяснение
(?<!\S)
Подтвердите a левая граница пробелов (?:
группа без захвата (@[^\s@]+)
захват группа 1 совпадение имени пользователя (?!\S)
утверждение границы правого пробела [^h]*
Совпадение 0+ раз с любым символом, кроме h
(?:
Группа без захвата h(?!ttp)[^h]*
Совпадение h
только в том случае, если за ним непосредственно не следует ttp
)*+
Закрыть группу и повторить 0+ раз, используя собственнический квантификатор
)?
Закройте группу и установите опцию для имени пользователя \K
Забудьте, что было найдено (
Захват группа 2 (?:https?|ftp|file)
Соответствует протоколу ://\S+
Соответствует ://
, за которым следует 1+ раз непробельный символ
)
Закрыть группу 2
Regex demo | Php демо
$string = <<<STR
Hello @username you need to check this http://github.com
and @username you need to https://stackoverflow.com/questions/ask
or https://stackoverflow.com
STR;
$pattern = "~(?<!\S)(?:(@[^\s@]+)(?!\S)[^h]*(?:h(?!ttp)[^h]*)*+)?\K((?:https?|ftp|file)://\S+)~";
$result = preg_replace_callback($pattern, function($m){
return sprintf('<a href="%s">%s</a>', $m[2],$m[1] !== "" ? $m[1] : $m[2]);
}, $string);
echo $result;
Выход
Hello @username you need to check this <a href="http://github.com">@username</a>
and @username you need to <a href="https://stackoverflow.com/questions/ask">@username</a>
or <a href="https://stackoverflow.com">https://stackoverflow.com</a>
Примечание
Если не может произойти @
между именем пользователя и URL вы можете использовать [^h@]*
вместо [^h]*