🦧

【Laravel】お知らせにハイパーリンク機能を実装する

2023/02/20に公開

前書き

先日、社内でLT発表会が開催されまして、その時に発表した「お知らせにハイパーリンク機能を実装する」という内容について今回はご紹介したいと思います。

背景としては、クライアント様より「お知らせの文章にハイパーリンク機能を実装してほしい」と依頼がいただいたので、Laravelでハイパーリンク機能を実装することになりました。

Markdown::parse

まず最初にハイパーリンクを実装する方法として思いついたのが、Mailableでも使用されているMarkdown::parse()です。

Markdown記法に準拠した書き方でお知らせを書き、表示させるタイミングでMarkdownからHTMLに変換させることが出来ます。

https://qiita.com/ntm718/items/009c9e611b5c88c1ec3b

使用例としては以下のような感じです。

<?php

use Illuminate\Mail\Markdown;

$text = "[Github](https://github.com)";

echo Markdown::parse($text);
// <a href="https://github.com">Github</a>
  • Laravelの標準機能でハイパーリンクが実現できる
  • 他のMarkdownにも対応
  • たった2行で実装できる

上記の理由から、Markdownファザードを採用するのが最適解なような気がしていました。

チームに提案したところ、さまざまな欠陥が。

先述した理由から、私は自信満々にMarkdownファザードを提案したところ、チームメンバーからは様々な懸念点を教えてもらいました。

その中でも大きな問題点は以下の2点です。

  • Markdownファザードでは、直打ちされたURLはリンクにならない
  • 予期せず見出しなどに変換される恐れがある

この2点はMarkdownファザードを採用するには大きな壁になります。

私はMarkdownファザード以外でハイパーリンクを実現できる方法を探しました。

正規表現でハイパーリンク機能を実装する

ハイパーリンク機能を実装するにあたり、Markdown記法は採用することにしました。リンク化する文言が判別しやすいためです。([TEXT](URL)という書き方)

ただし、ハイパーリンク以外のMarkdownの機能を実装したくありません。先述した通り、見出しの機能などは実装しない方法で実現したい。

そんな時、正規表現で以下の2種類を判別する方法を思いつきました。

  1. 直打ちのURL(https://~
  2. Markdown記法に則ったURL([TEXT](https://~)

この方法であれば、直打ちのURLをリンク化しつつハイパーリンクも実装可能です。

順に説明していきます。

<?php

$string = mb_ereg_replace(
    "((?<!]\()https?://[-_.!~*\'()a-zA-Z0-9;/?:@&=+$,%#]+)",
    "<a href="\1">\1</a>",
    $string
);

$pattern = "|\[(.*?)]\((https?://[-_.!~*\'()a-zA-Z0-9;/?:@&=+$,%#]+)\)|mis";
preg_match(
    $pattern,
    $string,
    $matches
);

if($matches){
    $str = $matches[1];
    $url = $matches[2];

    $tag = "<a href='$url'>$str</a>";

    $string = preg_replace(
        $pattern,
        $tag,
        $string
    );
}

リプレイスするコード

<?php

$string = mb_ereg_replace(
    "((?<!]\()https?://[-_.!~*\'()a-zA-Z0-9;/?:@&=+$,%#]+)",
    "<a href="\1">\1</a>",
    $string
);

ここでは、直貼りされたURLを判別してaタグに変換しています。

正規表現は「否定後読み」という方法で、Markdown記法で書かれたURLかどうかを判断しています。
https://~の形式の文字列とマッチして、httpの直前に](があるかどうかをどうかを見に行きます。

mb_ereg_replace()は第3引数に渡された文字列から、第1引数に渡した正規表現にマッチした部分を第2引数の形に置換するPHPのメソッドです。

https://www.php.net/manual/ja/function.mb-ereg-replace.php

<?php

$pattern = "|\[(.*?)]\((https?://[-_.!~*\'()a-zA-Z0-9;/?:@&=+$,%#]+)\)|mis";
preg_match(
    $pattern,
    $string,
    $matches
);

続いてのコードですが、ここではMarkdown記法に準拠したURLをURL部分と文字列部分で分けています。

大括弧[]の中身と括弧()の中身を正規表現でマッチさせ、$matchesの中に配列として格納しています。

preg_match()メソッドは、正規表現にマッチした文字列を第3引数に配列として格納するためのメソッドです。

https://www.php.net/manual/ja/function.preg-match.php

if($matches){
    $str = $matches[1];
    $url = $matches[2];

    $tag = "<a href='$url'>$str</a>";

    $string = preg_replace(
        $pattern,
        $tag,
        $string
    );
}

最後のコードでは、$matchesの中身があった時に$matchesindex=1に文字列、index=2にURLが格納されているので、分かりやすいように変数に代入しています。
変数に代入した文字列とURLを、aタグの形にして$tagに代入しています。

最後に、preg_replace()で正規表現にマッチした文字列と$tagを入れ替えています。

https://www.php.net/manual/ja/function.preg-replace.php

終わりに

今回正規表現で実現しましたが、もっとスマートな方法があるかもしれません。
知っている方いたら、教えていただけると嬉しいです。

正規表現についてはほとんど触れてこなかったので、もっと触る機会を増やしていかなければならないなと感じました。

Hajimari Tech Media

Discussion