[PHP]nl2br関数とは?改行をHTMLのタグに変換する方法

PHP

はじめに

Webアプリケーション開発で、ユーザーが入力したテキストの改行をHTMLで正しく表示したいとき、どのように処理していますか?PHPのnl2br関数は、テキスト内の改行文字(\n、\r\n、\r)を HTML の<br>タグに変換する非常に便利な関数です。

この記事では、nl2br関数の基本的な使い方から実践的な応用例まで、詳しく解説していきます。

nl2br関数とは?

nl2brは「New Line to Break」の略で、テキスト内の改行文字を HTML の<br>タグまたは<br />タグに変換する関数です。主にユーザー入力のテキストをWebページで表示する際に使用されます。

基本的な構文

string nl2br(string $string, bool $use_xhtml = true)

パラメータ

  • $string: 変換するテキスト文字列
  • $use_xhtml: XHTML準拠の<br />を使用するか(デフォルト: true)

戻り値

  • 改行文字が<br>または<br />タグに変換された文字列

基本的な使い方

1. シンプルな改行変換

<?php
$text = "こんにちは\nこれは2行目です\r\nこれは3行目です";

echo "変換前:\n";
echo $text;

echo "\n\n変換後:\n";
echo nl2br($text);

/*
変換前:
こんにちは
これは2行目です
これは3行目です

変換後:
こんにちは<br />
これは2行目です<br />
これは3行目です
*/
?>

2. XHTML形式とHTML形式

<?php
$text = "1行目\n2行目\n3行目";

// XHTML形式(デフォルト)
echo "XHTML形式:\n";
echo nl2br($text, true);
echo "\n\n";

// HTML形式
echo "HTML形式:\n";
echo nl2br($text, false);

/*
XHTML形式:
1行目<br />
2行目<br />
3行目

HTML形式:
1行目<br>
2行目<br>
3行目
*/
?>

3. 異なる改行文字の処理

<?php
// 様々な改行文字を含むテキスト
$text = "Unix改行\nWindows改行\r\nMac改行\r混合改行";

echo "元のテキスト(改行文字を可視化):\n";
echo str_replace(["\n", "\r\n", "\r"], ["\\n", "\\r\\n", "\\r"], $text);
echo "\n\n";

echo "nl2br変換後:\n";
echo nl2br($text);

/*
元のテキスト(改行文字を可視化):
Unix改行\nWindows改行\r\nMac改行\r混合改行

nl2br変換後:
Unix改行<br />
Windows改行<br />
Mac改行<br />
混合改行
*/
?>

実践的な使用例

1. ユーザー入力の表示

<?php
class TextDisplayer {
    public static function displayUserText($text, $options = []) {
        $options = array_merge([
            'escape_html' => true,
            'convert_newlines' => true,
            'use_xhtml' => true,
            'max_length' => null,
            'add_paragraphs' => false
        ], $options);
        
        // HTMLエスケープ
        if ($options['escape_html']) {
            $text = htmlspecialchars($text, ENT_QUOTES, 'UTF-8');
        }
        
        // 長さ制限
        if ($options['max_length'] && mb_strlen($text) > $options['max_length']) {
            $text = mb_substr($text, 0, $options['max_length']) . '...';
        }
        
        // 改行変換
        if ($options['convert_newlines']) {
            if ($options['add_paragraphs']) {
                // 連続する改行を段落に変換
                $text = self::convertToParagraphs($text);
            } else {
                // 単純にbrタグに変換
                $text = nl2br($text, $options['use_xhtml']);
            }
        }
        
        return $text;
    }
    
    private static function convertToParagraphs($text) {
        // 連続する改行で段落を分割
        $paragraphs = preg_split('/\n\s*\n/', trim($text));
        
        $result = '';
        foreach ($paragraphs as $paragraph) {
            $paragraph = trim($paragraph);
            if ($paragraph) {
                // 段落内の単一改行はbrタグに変換
                $paragraph = nl2br($paragraph, true);
                $result .= '<p>' . $paragraph . '</p>' . "\n";
            }
        }
        
        return $result;
    }
}

// 使用例
$userInput = "これは最初の段落です。\n改行を含んでいます。\n\nこれは2つ目の段落です。\nこちらも改行があります。";

echo "=== 基本表示 ===\n";
echo TextDisplayer::displayUserText($userInput);

echo "\n\n=== 段落付き表示 ===\n";
echo TextDisplayer::displayUserText($userInput, ['add_paragraphs' => true]);

echo "\n\n=== HTML形式 ===\n";
echo TextDisplayer::displayUserText($userInput, ['use_xhtml' => false]);
?>

2. フォーム処理とバリデーション

<?php
class FormProcessor {
    private $errors = [];
    
    public function processTextArea($name, $input, $rules = []) {
        $rules = array_merge([
            'required' => false,
            'max_length' => 1000,
            'min_lines' => null,
            'max_lines' => null,
            'allow_html' => false,
            'preserve_formatting' => true
        ], $rules);
        
        // 必須チェック
        if ($rules['required'] && empty(trim($input))) {
            $this->errors[$name] = 'この項目は必須です';
            return null;
        }
        
        if (empty(trim($input))) {
            return '';
        }
        
        // 長さチェック
        if ($rules['max_length'] && mb_strlen($input) > $rules['max_length']) {
            $this->errors[$name] = "最大{$rules['max_length']}文字まで入力できます";
            return null;
        }
        
        // 行数チェック
        $lines = explode("\n", $input);
        $lineCount = count($lines);
        
        if ($rules['min_lines'] && $lineCount < $rules['min_lines']) {
            $this->errors[$name] = "最低{$rules['min_lines']}行必要です";
            return null;
        }
        
        if ($rules['max_lines'] && $lineCount > $rules['max_lines']) {
            $this->errors[$name] = "最大{$rules['max_lines']}行まで入力できます";
            return null;
        }
        
        // HTMLエスケープとフォーマット処理
        $processed = $input;
        
        if (!$rules['allow_html']) {
            $processed = htmlspecialchars($processed, ENT_QUOTES, 'UTF-8');
        }
        
        if ($rules['preserve_formatting']) {
            $processed = nl2br($processed);
        }
        
        return $processed;
    }
    
    public function getErrors() {
        return $this->errors;
    }
    
    public function hasErrors() {
        return !empty($this->errors);
    }
    
    public function getFormattedErrors() {
        if (empty($this->errors)) {
            return '';
        }
        
        $html = '<div class="errors"><ul>';
        foreach ($this->errors as $field => $error) {
            $html .= '<li><strong>' . htmlspecialchars($field) . ':</strong> ' . htmlspecialchars($error) . '</li>';
        }
        $html .= '</ul></div>';
        
        return $html;
    }
}

// 使用例
$processor = new FormProcessor();

// サンプルフォームデータ
$formData = [
    'name' => '田中太郎',
    'message' => "こんにちは!\n\nお問い合わせがあります。\n詳細は以下の通りです。\n\n- 項目1\n- 項目2\n- 項目3",
    'comment' => "短いコメント"
];

echo "=== フォーム処理結果 ===\n";

$processedName = $processor->processTextArea('name', $formData['name'], [
    'required' => true,
    'max_length' => 50,
    'preserve_formatting' => false
]);

$processedMessage = $processor->processTextArea('message', $formData['message'], [
    'required' => true,
    'max_length' => 500,
    'min_lines' => 3,
    'max_lines' => 20
]);

$processedComment = $processor->processTextArea('comment', $formData['comment'], [
    'required' => false,
    'max_length' => 100
]);

if ($processor->hasErrors()) {
    echo "エラーが発生しました:\n";
    print_r($processor->getErrors());
} else {
    echo "名前: $processedName\n\n";
    echo "メッセージ:\n$processedMessage\n\n";
    echo "コメント: $processedComment\n";
}
?>

3. メール送信での改行処理

<?php
class EmailFormatter {
    public static function formatTextEmail($subject, $body, $options = []) {
        $options = array_merge([
            'line_length' => 72,
            'convert_html_breaks' => true,
            'add_signature' => true,
            'encoding' => 'UTF-8'
        ], $options);
        
        // HTMLのbrタグを改行文字に戻す
        if ($options['convert_html_breaks']) {
            $body = self::br2nl($body);
        }
        
        // 行の長さを調整
        if ($options['line_length']) {
            $body = self::wordWrap($body, $options['line_length']);
        }
        
        // 署名を追加
        if ($options['add_signature']) {
            $body .= "\n\n--\n送信元: システム自動送信";
        }
        
        return [
            'subject' => $subject,
            'body' => $body,
            'headers' => [
                'Content-Type' => 'text/plain; charset=' . $options['encoding'],
                'Content-Transfer-Encoding' => '8bit'
            ]
        ];
    }
    
    public static function formatHtmlEmail($subject, $body, $options = []) {
        $options = array_merge([
            'convert_newlines' => true,
            'add_css' => true,
            'encoding' => 'UTF-8'
        ], $options);
        
        // 改行をbrタグに変換
        if ($options['convert_newlines']) {
            $body = nl2br(htmlspecialchars($body, ENT_QUOTES, $options['encoding']));
        }
        
        // CSSスタイルを追加
        if ($options['add_css']) {
            $body = self::addEmailCSS($body);
        }
        
        // HTMLテンプレートで包む
        $htmlBody = self::wrapInHtmlTemplate($body, $subject);
        
        return [
            'subject' => $subject,
            'body' => $htmlBody,
            'headers' => [
                'Content-Type' => 'text/html; charset=' . $options['encoding'],
                'Content-Transfer-Encoding' => '8bit'
            ]
        ];
    }
    
    private static function br2nl($text) {
        return preg_replace('/<br\s*\/?>/i', "\n", $text);
    }
    
    private static function wordWrap($text, $length) {
        $lines = explode("\n", $text);
        $wrappedLines = [];
        
        foreach ($lines as $line) {
            if (mb_strlen($line) <= $length) {
                $wrappedLines[] = $line;
            } else {
                $words = explode(' ', $line);
                $currentLine = '';
                
                foreach ($words as $word) {
                    if (mb_strlen($currentLine . ' ' . $word) <= $length) {
                        $currentLine .= ($currentLine ? ' ' : '') . $word;
                    } else {
                        if ($currentLine) {
                            $wrappedLines[] = $currentLine;
                        }
                        $currentLine = $word;
                    }
                }
                
                if ($currentLine) {
                    $wrappedLines[] = $currentLine;
                }
            }
        }
        
        return implode("\n", $wrappedLines);
    }
    
    private static function addEmailCSS($body) {
        return '<div style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">' . 
               $body . 
               '</div>';
    }
    
    private static function wrapInHtmlTemplate($body, $title) {
        return '<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>' . htmlspecialchars($title) . '</title>
</head>
<body style="margin: 0; padding: 20px; background-color: #f5f5f5;">
    <div style="max-width: 600px; margin: 0 auto; background-color: white; padding: 20px; border-radius: 8px;">
        ' . $body . '
    </div>
</body>
</html>';
    }
}

// 使用例
$emailContent = "お客様へ\n\n平素よりお世話になっております。\n\nご注文の件でご連絡いたします。\n詳細は以下の通りです:\n\n- 商品名: サンプル商品\n- 数量: 2個\n- 金額: 3,000円\n\nご確認のほどよろしくお願いいたします。";

echo "=== テキストメール ===\n";
$textEmail = EmailFormatter::formatTextEmail('ご注文確認', $emailContent);
echo "件名: " . $textEmail['subject'] . "\n";
echo "本文:\n" . $textEmail['body'] . "\n\n";

echo "=== HTMLメール ===\n";
$htmlEmail = EmailFormatter::formatHtmlEmail('ご注文確認', $emailContent);
echo "件名: " . $htmlEmail['subject'] . "\n";
echo "本文:\n" . $htmlEmail['body'] . "\n";
?>

高度な使用例とパターン

1. マークダウン風テキスト処理

<?php
class SimpleMarkdownProcessor {
    public static function process($text, $options = []) {
        $options = array_merge([
            'convert_newlines' => true,
            'process_bold' => true,
            'process_italic' => true,
            'process_links' => true,
            'process_lists' => true,
            'escape_html' => true
        ], $options);
        
        if ($options['escape_html']) {
            $text = htmlspecialchars($text, ENT_QUOTES, 'UTF-8');
        }
        
        // 太字処理: **text** -> <strong>text</strong>
        if ($options['process_bold']) {
            $text = preg_replace('/\*\*(.*?)\*\*/', '<strong>$1</strong>', $text);
        }
        
        // イタリック処理: *text* -> <em>text</em>
        if ($options['process_italic']) {
            $text = preg_replace('/\*(.*?)\*/', '<em>$1</em>', $text);
        }
        
        // リンク処理: [text](url) -> <a href="url">text</a>
        if ($options['process_links']) {
            $text = preg_replace('/\[([^\]]+)\]\(([^)]+)\)/', '<a href="$2">$1</a>', $text);
        }
        
        // リスト処理
        if ($options['process_lists']) {
            $text = self::processLists($text);
        }
        
        // 改行をbrタグに変換(最後に実行)
        if ($options['convert_newlines']) {
            $text = nl2br($text);
        }
        
        return $text;
    }
    
    private static function processLists($text) {
        $lines = explode("\n", $text);
        $result = [];
        $inList = false;
        
        foreach ($lines as $line) {
            $trimmed = trim($line);
            
            if (preg_match('/^[*\-+]\s+(.+)/', $trimmed, $matches)) {
                if (!$inList) {
                    $result[] = '<ul>';
                    $inList = true;
                }
                $result[] = '<li>' . $matches[1] . '</li>';
            } else {
                if ($inList) {
                    $result[] = '</ul>';
                    $inList = false;
                }
                $result[] = $line;
            }
        }
        
        if ($inList) {
            $result[] = '</ul>';
        }
        
        return implode("\n", $result);
    }
}

// 使用例
$markdownText = "# タイトル

これは**重要**な*テキスト*です。

詳細については[こちら](https://example.com)をご覧ください。

リストは以下の通りです:

* 項目1
* 項目2  
* 項目3

以上です。";

echo "=== マークダウン処理結果 ===\n";
echo SimpleMarkdownProcessor::process($markdownText);
?>

2. コメントシステム

<?php
class CommentSystem {
    private $comments = [];
    
    public function addComment($author, $content, $parentId = null) {
        $commentId = uniqid();
        
        $comment = [
            'id' => $commentId,
            'author' => htmlspecialchars($author, ENT_QUOTES, 'UTF-8'),
            'content' => $this->processContent($content),
            'parent_id' => $parentId,
            'created_at' => date('Y-m-d H:i:s'),
            'replies' => []
        ];
        
        if ($parentId && isset($this->comments[$parentId])) {
            $this->comments[$parentId]['replies'][$commentId] = $comment;
        } else {
            $this->comments[$commentId] = $comment;
        }
        
        return $commentId;
    }
    
    private function processContent($content) {
        // 不適切な内容をフィルタリング
        $content = $this->filterContent($content);
        
        // HTMLエスケープ
        $content = htmlspecialchars($content, ENT_QUOTES, 'UTF-8');
        
        // 改行をbrタグに変換
        $content = nl2br($content);
        
        // URLを自動リンク化
        $content = $this->autoLinkUrls($content);
        
        // @メンション処理
        $content = $this->processMentions($content);
        
        return $content;
    }
    
    private function filterContent($content) {
        // 禁止用語のフィルタリング(実際にはより高度な処理が必要)
        $bannedWords = ['spam', '不適切', '禁止用語'];
        
        foreach ($bannedWords as $word) {
            $content = str_ireplace($word, str_repeat('*', mb_strlen($word)), $content);
        }
        
        return $content;
    }
    
    private function autoLinkUrls($content) {
        $pattern = '/https?:\/\/[^\s<>"\']+/';
        
        return preg_replace_callback($pattern, function($matches) {
            $url = $matches[0];
            $displayUrl = mb_strlen($url) > 50 ? mb_substr($url, 0, 47) . '...' : $url;
            return '<a href="' . $url . '" target="_blank" rel="noopener">' . $displayUrl . '</a>';
        }, $content);
    }
    
    private function processMentions($content) {
        return preg_replace('/@(\w+)/', '<span class="mention">@$1</span>', $content);
    }
    
    public function displayComments() {
        foreach ($this->comments as $comment) {
            $this->displayComment($comment);
        }
    }
    
    private function displayComment($comment, $level = 0) {
        $indent = str_repeat('  ', $level);
        $replyClass = $level > 0 ? ' reply' : '';
        
        echo $indent . '<div class="comment' . $replyClass . '">' . "\n";
        echo $indent . '  <div class="comment-header">' . "\n";
        echo $indent . '    <strong>' . $comment['author'] . '</strong> ';
        echo '<span class="comment-date">' . $comment['created_at'] . '</span>' . "\n";
        echo $indent . '  </div>' . "\n";
        echo $indent . '  <div class="comment-content">' . "\n";
        echo $indent . '    ' . $comment['content'] . "\n";
        echo $indent . '  </div>' . "\n";
        
        // 返信を表示
        foreach ($comment['replies'] as $reply) {
            $this->displayComment($reply, $level + 1);
        }
        
        echo $indent . '</div>' . "\n";
    }
    
    public function getCommentsAsJson() {
        return json_encode($this->comments, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
    }
}

// 使用例
$commentSystem = new CommentSystem();

// コメントを追加
$comment1 = $commentSystem->addComment('田中太郎', "とても良い記事ですね!\n\n特に最後の部分が参考になりました。\n\n@佐藤さん はどう思いますか?");

$comment2 = $commentSystem->addComment('佐藤花子', "私も同感です。\n詳細は https://example.com/detail で確認できます。");

$reply1 = $commentSystem->addComment('山田次郎', "お二人とも、貴重なご意見ありがとうございます。\n\n追加の情報があれば教えてください。", $comment1);

echo "=== コメント表示 ===\n";
$commentSystem->displayComments();

echo "\n=== JSON出力 ===\n";
echo $commentSystem->getCommentsAsJson();
?>

3. ログ表示システム

<?php
class LogDisplaySystem {
    private $logEntries = [];
    
    public function addLogEntry($level, $message, $context = []) {
        $this->logEntries[] = [
            'timestamp' => microtime(true),
            'level' => $level,
            'message' => $message,
            'context' => $context,
            'formatted_time' => date('Y-m-d H:i:s')
        ];
    }
    
    public function displayLogs($options = []) {
        $options = array_merge([
            'format' => 'html', // html, text, json
            'level_filter' => null,
            'limit' => null,
            'reverse_order' => true,
            'show_context' => true,
            'convert_newlines' => true
        ], $options);
        
        $logs = $this->logEntries;
        
        // レベルフィルタ
        if ($options['level_filter']) {
            $logs = array_filter($logs, function($log) use ($options) {
                return $log['level'] === $options['level_filter'];
            });
        }
        
        // 順序
        if ($options['reverse_order']) {
            $logs = array_reverse($logs);
        }
        
        // 制限
        if ($options['limit']) {
            $logs = array_slice($logs, 0, $options['limit']);
        }
        
        // フォーマット別表示
        switch ($options['format']) {
            case 'html':
                return $this->formatAsHtml($logs, $options);
            case 'text':
                return $this->formatAsText($logs, $options);
            case 'json':
                return $this->formatAsJson($logs, $options);
            default:
                return $this->formatAsHtml($logs, $options);
        }
    }
    
    private function formatAsHtml($logs, $options) {
        $html = '<div class="log-container">';
        
        foreach ($logs as $log) {
            $levelClass = 'log-' . strtolower($log['level']);
            $message = htmlspecialchars($log['message'], ENT_QUOTES, 'UTF-8');
            
            if ($options['convert_newlines']) {
                $message = nl2br($message);
            }
            
            $html .= '<div class="log-entry ' . $levelClass . '">';
            $html .= '<span class="log-time">' . $log['formatted_time'] . '</span>';
            $html .= '<span class="log-level">[' . $log['level'] . ']</span>';
            $html .= '<span class="log-message">' . $message . '</span>';
            
            if ($options['show_context'] && !empty($log['context'])) {
                $html .= '<div class="log-context">';
                $html .= '<strong>Context:</strong> ';
                $html .= '<pre>' . htmlspecialchars(json_encode($log['context'], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)) . '</pre>';
                $html .= '</div>';
            }
            
            $html .= '</div>';
        }
        
        $html .= '</div>';
        
        // CSSを追加
        $css = '<style>
.log-container { font-family: monospace; }
.log-entry { margin: 5px 0; padding: 10px; border-left: 4px solid #ccc; }
.log-error { border-left-color: #e74c3c; background-color: #fdf2f2; }
.log-warning { border-left-color: #f39c12; background-color: #fef9e7; }
.log-info { border-left-color: #3498db; background-color: #eaf2f8; }
.log-debug { border-left-color: #95a5a6; background-color: #f8f9fa; }
.log-time { color: #666; margin-right: 10px; }
.log-level { font-weight: bold; margin-right: 10px; }
.log-context { margin-top: 10px; padding-top: 10px; border-top: 1px solid #eee; }
.log-context pre { background: #f5f5f5; padding: 5px; border-radius: 3px; overflow-x: auto; }
</style>';
        
        return $css . $html;
    }
    
    private function formatAsText($logs, $options) {
        $text = '';
        
        foreach ($logs as $log) {
            $message = $log['message'];
            
            $text .= '[' . $log['formatted_time'] . '] ';
            $text .= $log['level'] . ': ';
            $text .= $message;
            
            if ($options['show_context'] && !empty($log['context'])) {
                $text .= ' | Context: ' . json_encode($log['context'], JSON_UNESCAPED_UNICODE);
            }
            
            $text .= "\n";
        }
        
        return $text;
    }
    
    private function formatAsJson($logs, $options) {
        return json_encode($logs, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
    }
    
    public function exportLogs($filename, $format = 'text') {
        $content = $this->displayLogs(['format' => $format]);
        
        if ($format === 'html') {
            $content = '<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>ログエクスポート</title>
</head>
<body>
' . $content . '
</body>
</html>';
        }
        
        file_put_contents($filename, $content);
        
        return filesize($filename);
    }
}

// 使用例
$logSystem = new LogDisplaySystem();

// ログエントリを追加
$logSystem->addLogEntry('ERROR', "データベースとの接続に失敗しました。\n\n原因:\n- 接続タイムアウト\n- 認証エラー\n- ネットワーク問題\n\n対処方法を確認してください。", ['error_code' => 'DB_CONNECTION_FAILED', 'retry_count' => 3]);

$logSystem->addLogEntry('DEBUG', "ユーザーセッションを作成しました。\nセッションID: abc123\nユーザーID: 456", ['session_id' => 'abc123', 'user_id' => 456]);

echo "=== HTML形式でのログ表示 ===\n";
echo $logSystem->displayLogs(['format' => 'html', 'limit' => 10]);

echo "\n\n=== テキスト形式でのログ表示 ===\n";
echo $logSystem->displayLogs(['format' => 'text', 'level_filter' => 'ERROR']);

// ログファイルとしてエクスポート
$fileSize = $logSystem->exportLogs('exported_logs.html', 'html');
echo "\n\nログファイルをエクスポートしました: exported_logs.html ($fileSize bytes)\n";
?>

セキュリティとベストプラクティス

1. XSS(クロスサイトスクリプティング)対策

<?php
class SecureTextProcessor {
    public static function safeNl2br($text, $options = []) {
        $options = array_merge([
            'escape_html' => true,
            'allow_safe_tags' => false,
            'safe_tags' => ['b', 'i', 'u', 'em', 'strong'],
            'max_length' => null,
            'remove_javascript' => true,
            'use_xhtml' => true
        ], $options);
        
        // 長さ制限
        if ($options['max_length'] && mb_strlen($text) > $options['max_length']) {
            $text = mb_substr($text, 0, $options['max_length']) . '...';
        }
        
        // JavaScriptの除去
        if ($options['remove_javascript']) {
            $text = self::removeJavaScript($text);
        }
        
        // HTMLエスケープ(安全なタグを許可しない場合)
        if ($options['escape_html'] && !$options['allow_safe_tags']) {
            $text = htmlspecialchars($text, ENT_QUOTES, 'UTF-8');
        }
        
        // 安全なタグのみを許可
        if ($options['allow_safe_tags']) {
            $text = self::allowOnlySafeTags($text, $options['safe_tags']);
        }
        
        // 改行をbrタグに変換
        $text = nl2br($text, $options['use_xhtml']);
        
        return $text;
    }
    
    private static function removeJavaScript($text) {
        // JavaScriptのイベントハンドラを除去
        $text = preg_replace('/on\w+\s*=\s*["\'][^"\']*["\']/i', '', $text);
        
        // javascript: プロトコルを除去
        $text = preg_replace('/javascript\s*:/i', '', $text);
        
        // <script>タグを除去
        $text = preg_replace('/<script[^>]*>.*?<\/script>/is', '', $text);
        
        return $text;
    }
    
    private static function allowOnlySafeTags($text, $safeTags) {
        // 許可されたタグ以外をエスケープ
        $allowedTags = '<' . implode('><', $safeTags) . '>';
        return strip_tags($text, $allowedTags);
    }
    
    public static function validateInput($text, $rules = []) {
        $errors = [];
        
        // 必須チェック
        if (isset($rules['required']) && $rules['required'] && empty(trim($text))) {
            $errors[] = '必須項目です';
        }
        
        // 長さチェック
        if (isset($rules['max_length']) && mb_strlen($text) > $rules['max_length']) {
            $errors[] = "最大{$rules['max_length']}文字まで入力可能です";
        }
        
        if (isset($rules['min_length']) && mb_strlen($text) < $rules['min_length']) {
            $errors[] = "最低{$rules['min_length']}文字必要です";
        }
        
        // 不正なスクリプトチェック
        if (preg_match('/<script[^>]*>/i', $text)) {
            $errors[] = '不正なスクリプトが含まれています';
        }
        
        // 改行数チェック
        if (isset($rules['max_lines'])) {
            $lineCount = substr_count($text, "\n") + 1;
            if ($lineCount > $rules['max_lines']) {
                $errors[] = "最大{$rules['max_lines']}行まで入力可能です";
            }
        }
        
        return $errors;
    }
    
    public static function sanitizeForDatabase($text) {
        // データベース用のサニタイズ
        return trim(preg_replace('/\s+/', ' ', $text));
    }
}

// 使用例
echo "=== セキュリティテスト ===\n";

$maliciousInput = "こんにちは\n<script>alert('XSS')</script>\nこれは危険なテキストです\n<b>太字</b>は大丈夫";

echo "元のテキスト:\n";
echo $maliciousInput . "\n\n";

echo "安全な処理(HTMLエスケープ):\n";
echo SecureTextProcessor::safeNl2br($maliciousInput) . "\n\n";

echo "安全なタグを許可:\n";
echo SecureTextProcessor::safeNl2br($maliciousInput, [
    'allow_safe_tags' => true,
    'safe_tags' => ['b', 'i', 'strong', 'em']
]) . "\n\n";

// バリデーションテスト
$validationErrors = SecureTextProcessor::validateInput($maliciousInput, [
    'required' => true,
    'max_length' => 100,
    'max_lines' => 5
]);

if (!empty($validationErrors)) {
    echo "バリデーションエラー:\n";
    foreach ($validationErrors as $error) {
        echo "- $error\n";
    }
}
?>

2. パフォーマンス最適化

<?php
class OptimizedTextProcessor {
    private static $cache = [];
    private static $cacheEnabled = true;
    
    public static function fastNl2br($text, $useXhtml = true, $useCache = true) {
        if (!$useCache || !self::$cacheEnabled) {
            return self::processText($text, $useXhtml);
        }
        
        $cacheKey = md5($text . ($useXhtml ? '1' : '0'));
        
        if (isset(self::$cache[$cacheKey])) {
            return self::$cache[$cacheKey];
        }
        
        $result = self::processText($text, $useXhtml);
        
        // キャッシュサイズを制限
        if (count(self::$cache) > 1000) {
            self::$cache = array_slice(self::$cache, -500, null, true);
        }
        
        self::$cache[$cacheKey] = $result;
        return $result;
    }
    
    private static function processText($text, $useXhtml) {
        // 最適化された改行変換
        $br = $useXhtml ? '<br />' : '<br>';
        
        // 一度ですべての改行パターンを変換
        return str_replace(["\r\n", "\r", "\n"], $br, $text);
    }
    
    public static function batchProcess($textArray, $useXhtml = true) {
        $results = [];
        $br = $useXhtml ? '<br />' : '<br>';
        
        foreach ($textArray as $key => $text) {
            $results[$key] = str_replace(["\r\n", "\r", "\n"], $br, $text);
        }
        
        return $results;
    }
    
    public static function streamProcess($inputStream, $outputStream, $useXhtml = true) {
        $br = $useXhtml ? '<br />' : '<br>';
        $processed = 0;
        
        while (($line = fgets($inputStream)) !== false) {
            $processedLine = str_replace(["\r\n", "\r", "\n"], $br, $line);
            fwrite($outputStream, $processedLine);
            $processed++;
        }
        
        return $processed;
    }
    
    public static function benchmarkMethods($text, $iterations = 1000) {
        $methods = [
            'nl2br' => function($text) { return nl2br($text); },
            'fastNl2br' => function($text) { return self::fastNl2br($text); },
            'str_replace' => function($text) { return str_replace(["\r\n", "\r", "\n"], '<br />', $text); }
        ];
        
        $results = [];
        
        foreach ($methods as $name => $method) {
            $start = microtime(true);
            
            for ($i = 0; $i < $iterations; $i++) {
                $method($text);
            }
            
            $results[$name] = microtime(true) - $start;
        }
        
        return $results;
    }
    
    public static function clearCache() {
        self::$cache = [];
    }
    
    public static function setCacheEnabled($enabled) {
        self::$cacheEnabled = $enabled;
    }
    
    public static function getCacheStats() {
        return [
            'cache_size' => count(self::$cache),
            'cache_enabled' => self::$cacheEnabled,
            'memory_usage' => memory_get_usage()
        ];
    }
}

// パフォーマンステスト
echo "=== パフォーマンステスト ===\n";

$testText = str_repeat("テスト行1\nテスト行2\r\nテスト行3\r", 100);

$benchmarkResults = OptimizedTextProcessor::benchmarkMethods($testText, 1000);

echo "処理時間比較 (1000回実行):\n";
$fastest = min($benchmarkResults);

foreach ($benchmarkResults as $method => $time) {
    $relative = round($time / $fastest, 2);
    echo sprintf("%-15s: %8.4f秒 (%sx)\n", $method, $time, $relative);
}

echo "\n=== キャッシュ統計 ===\n";
print_r(OptimizedTextProcessor::getCacheStats());

// 大量データのバッチ処理テスト
echo "\n=== バッチ処理テスト ===\n";
$batchData = [];
for ($i = 0; $i < 1000; $i++) {
    $batchData["item_$i"] = "行1\n行2\r\n行3\r項目 $i";
}

$start = microtime(true);
$batchResults = OptimizedTextProcessor::batchProcess($batchData);
$batchTime = microtime(true) - $start;

echo "バッチ処理: 1000件を " . number_format($batchTime, 4) . "秒で処理\n";
echo "1件あたり: " . number_format($batchTime / 1000 * 1000000, 2) . "マイクロ秒\n";
?>

3. 国際化対応

<?php
class InternationalTextProcessor {
    private $locale;
    private $encoding;
    
    public function __construct($locale = 'ja_JP', $encoding = 'UTF-8') {
        $this->locale = $locale;
        $this->encoding = $encoding;
    }
    
    public function processText($text, $options = []) {
        $options = array_merge([
            'convert_newlines' => true,
            'normalize_unicode' => true,
            'handle_rtl' => false,
            'preserve_whitespace' => false,
            'use_xhtml' => true
        ], $options);
        
        // Unicode正規化
        if ($options['normalize_unicode'] && function_exists('normalizer_normalize')) {
            $text = normalizer_normalize($text, Normalizer::FORM_C);
        }
        
        // 右から左への言語(アラビア語、ヘブライ語等)対応
        if ($options['handle_rtl']) {
            $text = $this->handleRtlText($text);
        }
        
        // 空白の処理
        if (!$options['preserve_whitespace']) {
            $text = $this->normalizeWhitespace($text);
        }
        
        // 改行変換
        if ($options['convert_newlines']) {
            $text = nl2br($text, $options['use_xhtml']);
        }
        
        return $text;
    }
    
    private function handleRtlText($text) {
        // RTL(右から左)テキストの検出
        if (preg_match('/[\x{0590}-\x{05ff}\x{0600}-\x{06ff}]/u', $text)) {
            return '<div dir="rtl">' . $text . '</div>';
        }
        
        return $text;
    }
    
    private function normalizeWhitespace($text) {
        // 全角・半角空白の統一
        $text = preg_replace('/[\x{00A0}\x{3000}]/u', ' ', $text);
        
        // 連続する空白を1つにまとめる(改行は除く)
        $text = preg_replace('/[ \t]+/', ' ', $text);
        
        return $text;
    }
    
    public function detectLanguage($text) {
        // 簡単な言語検出(実際にはより高度なライブラリを使用)
        $patterns = [
            'japanese' => '/[\x{3040}-\x{309f}\x{30a0}-\x{30ff}\x{4e00}-\x{9faf}]/u',
            'korean' => '/[\x{1100}-\x{11ff}\x{3130}-\x{318f}\x{ac00}-\x{d7af}]/u',
            'chinese' => '/[\x{4e00}-\x{9fff}]/u',
            'arabic' => '/[\x{0600}-\x{06ff}]/u',
            'hebrew' => '/[\x{0590}-\x{05ff}]/u',
            'cyrillic' => '/[\x{0400}-\x{04ff}]/u'
        ];
        
        foreach ($patterns as $language => $pattern) {
            if (preg_match($pattern, $text)) {
                return $language;
            }
        }
        
        return 'latin';
    }
    
    public function formatForEmail($text, $emailType = 'html') {
        $language = $this->detectLanguage($text);
        
        if ($emailType === 'html') {
            $text = htmlspecialchars($text, ENT_QUOTES, $this->encoding);
            $text = nl2br($text, true);
            
            // 言語に応じたフォント指定
            $fontFamily = $this->getFontFamily($language);
            $text = '<div style="font-family: ' . $fontFamily . '; line-height: 1.6;">' . $text . '</div>';
        } else {
            // テキストメールの場合は改行を保持
            $text = $this->wordWrapMultibyte($text, 72);
        }
        
        return $text;
    }
    
    private function getFontFamily($language) {
        $fonts = [
            'japanese' => 'Hiragino Kaku Gothic ProN, Meiryo, sans-serif',
            'korean' => 'Malgun Gothic, Dotum, sans-serif',
            'chinese' => 'Microsoft YaHei, SimHei, sans-serif',
            'arabic' => 'Tahoma, Arial Unicode MS, sans-serif',
            'hebrew' => 'David, Arial Unicode MS, sans-serif',
            'cyrillic' => 'Arial, Helvetica, sans-serif',
            'latin' => 'Arial, Helvetica, sans-serif'
        ];
        
        return $fonts[$language] ?? $fonts['latin'];
    }
    
    private function wordWrapMultibyte($text, $width) {
        $lines = explode("\n", $text);
        $wrappedLines = [];
        
        foreach ($lines as $line) {
            if (mb_strlen($line, $this->encoding) <= $width) {
                $wrappedLines[] = $line;
            } else {
                $words = explode(' ', $line);
                $currentLine = '';
                
                foreach ($words as $word) {
                    if (mb_strlen($currentLine . ' ' . $word, $this->encoding) <= $width) {
                        $currentLine .= ($currentLine ? ' ' : '') . $word;
                    } else {
                        if ($currentLine) {
                            $wrappedLines[] = $currentLine;
                        }
                        $currentLine = $word;
                    }
                }
                
                if ($currentLine) {
                    $wrappedLines[] = $currentLine;
                }
            }
        }
        
        return implode("\n", $wrappedLines);
    }
}

// 使用例
echo "=== 国際化対応テスト ===\n";

$processor = new InternationalTextProcessor();

$multilingualText = "Hello World!\nこんにちは世界!\n안녕하세요 세계!\n你好世界!\nمرحبا بالعالم";

echo "多言語テキスト処理:\n";
echo $processor->processText($multilingualText);

echo "\n\n言語検出:\n";
$texts = [
    "これは日本語のテキストです。",
    "This is English text.",
    "이것은 한국어 텍스트입니다.",
    "这是中文文本。",
    "هذا نص عربي"
];

foreach ($texts as $text) {
    $language = $processor->detectLanguage($text);
    echo "$text -> $language\n";
}

echo "\n\nメール形式での処理:\n";
$emailText = "件名: お問い合わせ\n\n平素よりお世話になっております。\n\nご質問がございます。\nよろしくお願いいたします。";
echo $processor->formatForEmail($emailText, 'html');
?>

トラブルシューティング

よくある問題と解決法

<?php
class Nl2brTroubleshooter {
    public static function diagnoseText($text) {
        echo "=== テキスト診断 ===\n";
        echo "文字数: " . mb_strlen($text) . "\n";
        echo "バイト数: " . strlen($text) . "\n";
        echo "エンコーディング: " . mb_detect_encoding($text) . "\n";
        
        // 改行文字の分析
        $lf = substr_count($text, "\n");
        $cr = substr_count($text, "\r");
        $crlf = substr_count($text, "\r\n");
        $realCr = $cr - $crlf; // \r\nでカウントされた\rを除く
        
        echo "改行文字:\n";
        echo "  LF (\\n): $lf\n";
        echo "  CR (\\r): $realCr\n";
        echo "  CRLF (\\r\\n): $crlf\n";
        
        // 特殊文字のチェック
        $specialChars = [
            'タブ' => "\t",
            '全角スペース' => ' ',
            'ノーブレークスペース' => "\xA0"
        ];
        
        foreach ($specialChars as $name => $char) {
            $count = substr_count($text, $char);
            if ($count > 0) {
                echo "  $name: {$count}個\n";
            }
        }
        
        // HTMLタグのチェック
        if (preg_match_all('/<[^>]+>/', $text, $matches)) {
            echo "HTMLタグ: " . count($matches[0]) . "個\n";
            $tags = array_unique($matches[0]);
            echo "  " . implode(', ', array_slice($tags, 0, 5));
            if (count($tags) > 5) {
                echo " ...他" . (count($tags) - 5) . "個";
            }
            echo "\n";
        }
        
        echo "\n";
    }
    
    public static function fixCommonIssues($text) {
        echo "=== 一般的な問題の修正 ===\n";
        
        $original = $text;
        $fixes = [];
        
        // Windows改行を統一
        if (strpos($text, "\r\n") !== false) {
            $text = str_replace("\r\n", "\n", $text);
            $fixes[] = "Windows改行(\\r\\n)をUnix改行(\\n)に統一";
        }
        
        // Mac改行を統一
        if (strpos($text, "\r") !== false) {
            $text = str_replace("\r", "\n", $text);
            $fixes[] = "Mac改行(\\r)をUnix改行(\\n)に統一";
        }
        
        // 連続する改行を制限
        if (preg_match('/\n{3,}/', $text)) {
            $text = preg_replace('/\n{3,}/', "\n\n", $text);
            $fixes[] = "3つ以上の連続改行を2つに制限";
        }
        
        // 行末の空白を除去
        if (preg_match('/[ \t]+$/m', $text)) {
            $text = preg_replace('/[ \t]+$/m', '', $text);
            $fixes[] = "行末の空白を除去";
        }
        
        // 全角スペースを半角に変換(オプション)
        $fullWidthSpaces = substr_count($text, ' ');
        if ($fullWidthSpaces > 0) {
            // この変換は慎重に行う必要がある
            echo "注意: 全角スペース({$fullWidthSpaces}個)が検出されました。\n";
            echo "必要に応じて手動で確認してください。\n";
        }
        
        if (empty($fixes)) {
            echo "修正が必要な問題は見つかりませんでした。\n";
        } else {
            echo "実行した修正:\n";
            foreach ($fixes as $fix) {
                echo "- $fix\n";
            }
        }
        
        echo "\n";
        return $text;
    }
    
    public static function compareResults($text) {
        echo "=== 変換結果の比較 ===\n";
        
        $methods = [
            'nl2br (XHTML)' => nl2br($text, true),
            'nl2br (HTML)' => nl2br($text, false),
            'str_replace' => str_replace(["\r\n", "\r", "\n"], '<br />', $text),
            'preg_replace' => preg_replace('/\r\n|\r|\n/', '<br />', $text)
        ];
        
        foreach ($methods as $method => $result) {
            echo "$method:\n";
            echo "  結果: " . htmlspecialchars($result) . "\n";
            echo "  長さ: " . mb_strlen($result) . "文字\n\n";
        }
    }
    
    public static function testWithProblematicData() {
        echo "=== 問題のあるデータでのテスト ===\n";
        
        $problematicTexts = [
            'mixed_newlines' => "Unix\nWindows\r\nMac\rMixed",
            'html_content' => "HTML含有<script>alert('test')</script>\n改行あり",
            'unicode_text' => "Unicode文字\n絵文字😀\n特殊文字①②③",
            'empty_lines' => "空行テスト\n\n\n\n複数空行",
            'trailing_spaces' => "行末空白   \n次の行\t\tタブあり",
            'false_value' => "値にfalseを含む\n" . false . "\n続き"
        ];
        
        foreach ($problematicTexts as $type => $text) {
            echo "--- $type ---\n";
            self::diagnoseText($text);
            
            $fixed = self::fixCommonIssues($text);
            
            echo "nl2br変換結果:\n";
            echo htmlspecialchars(nl2br($fixed)) . "\n\n";
        }
    }
}

// トラブルシューティングの実行
$problematicText = "問題のあるテキスト\r\n\r\n\r\nWindows改行\r\n行末空白あり   \nタブ文字\t含む\nHTML<b>タグ</b>あり";

Nl2brTroubleshooter::diagnoseText($problematicText);
$fixedText = Nl2brTroubleshooter::fixCommonIssues($problematicText);
Nl2brTroubleshooter::compareResults($fixedText);

// 問題のあるデータでの包括的テスト
Nl2brTroubleshooter::testWithProblematicData();
?>

まとめ

nl2br関数は、Webアプリケーション開発において非常に重要な機能を提供する関数です。適切に使用することで、ユーザー入力のテキストを安全かつ見やすい形でWebページに表示できます。

重要なポイント

  • 基本機能: 改行文字(\n、\r、\r\n)をHTMLの<br>タグに変換
  • XHTMLオプション: 第2パラメータで<br>または<br />を選択可能
  • セキュリティ: HTMLエスケープとの組み合わせが重要
  • パフォーマンス: 大量データではキャッシュや最適化を検討

ベストプラクティス

  1. セキュリティ: 必ずhtmlspecialchars()と組み合わせて使用
  2. バリデーション: ユーザー入力の検証を適切に実装
  3. パフォーマンス: 大量処理ではキャッシュ機能を活用
  4. 国際化: 多言語対応を考慮した実装
  5. テスト: 様々な改行パターンでテストを実施

推奨される使用場面

  1. ユーザー投稿: ブログ投稿、コメント、レビューの表示
  2. フォーム処理: テキストエリアの内容を表示
  3. メール送信: HTMLメールでのテキスト整形
  4. ログ表示: システムログの見やすい表示

nl2br関数を理解し適切に使用することで、ユーザビリティが高く安全なWebアプリケーションを開発できます。特にユーザー生成コンテンツを扱う現代のWebアプリケーションでは必須の知識と言えるでしょう。

タイトルとURLをコピーしました