首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 相關知識

PHP 產生 RSS 2.0 訂閱源完整實作:標準規範、動態生成、快取、分類與 Podcast 擴充

     編輯:PHP 相關知識

RSS (Really Simple Syndication) 仍是內容分發標準格式。PHP 可動態生成符合 RSS 2.0 規範的 XML,支援部落格、新聞、Podcast 等場景。本文提供標準類別實作、資料庫整合、HTTP 快取頭、iTunes Podcast 擴充命名空間與驗證工具。

RSS 2.0 核心結構

<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
     xmlns:content="https://purl.org/rss/1.0/modules/content/"
     xmlns:atom="https://www.w3.org/2005/Atom"
     xmlns:itunes="https://www.itunes.com/dtds/podcast-1.0.dtd">
  <channel>
    <title>網站標題</title>
    <link>https://example.com</link>
    <description>網站描述</description>
    <language>zh-TW</language>
    <lastBuildDate>Mon, 01 Jan 2024 00:00:00 +0800</lastBuildDate>
    <atom:link href="https://example.com/rss.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>文章標題</title>
      <link>https://example.com/post/1</link>
      <guid isPermaLink="true">https://example.com/post/1</guid>
      <description><![CDATA[文章摘要或全文 HTML]]></description>
      <pubDate>Mon, 01 Jan 2024 00:00:00 +0800</pubDate>
      <category>分類名稱</category>
    </item>
  </channel>
</rss>

PHP 產生器類別 (支援鏈式呼叫、多項目、快取)

class RssFeed
{
    private string $title;
    private string $link;
    private string $description;
    private string $language = 'zh-TW';
    private string $copyright = '';
    private array $items = [];
    private array $categories = [];
    private string $imageUrl = '';
    private int $ttl = 60; // 分鐘
    private array $namespaces = [];

    // 設定方法 (鏈式)
    public function title(string $t): self { $this->title = $t; return $this; }
    public function link(string $l): self { $this->link = $l; return $this; }
    public function description(string $d): self { $this->description = $d; return $this; }
    public function language(string $l): self { $this->language = $l; return $this; }
    public function copyright(string $c): self { $this->copyright = $c; return $this; }
    public function ttl(int $m): self { $this->ttl = $m; return $this; }
    public function image(string $url, string $title = '', string $link = ''): self {
        $this->imageUrl = $url;
        return $this;
    }
    public function addNamespace(string $prefix, string $uri): self {
        $this->namespaces[$prefix] = $uri;
        return $this;
    }

    // 新增項目
    public function addItem(array $item): self
    {
        $defaults = [
            'title' => '',
            'link' => '',
            'guid' => '',
            'description' => '',
            'pubDate' => null, // DateTimeImmutable 或字串
            'categories' => [],
            'author' => '',
            'enclosure' => null, // ['url', 'type', 'length'] for Podcast
        ];
        $this->items[] = array_merge($defaults, $item);
        return $this;
    }

    // 產生 XML 字串
    public function render(): string
    {
        $dom = new DOMDocument('1.0', 'UTF-8');
        $dom->formatOutput = true;

        $rss = $dom->createElement('rss');
        $rss->setAttribute('version', '2.0');
        foreach ($this->namespaces as $prefix => $uri) {
            $rss->setAttribute("xmlns:$prefix", $uri);
        }
        $dom->appendChild($rss);

        $channel = $dom->createElement('channel');
        $rss->appendChild($channel);

        // Channel 必要元素
        $this->appendElement($dom, $channel, 'title', $this->title);
        $this->appendElement($dom, $channel, 'link', $this->link);
        $this->appendElement($dom, $channel, 'description', $this->description);
        $this->appendElement($dom, $channel, 'language', $this->language);
        if ($this->copyright) $this->appendElement($dom, $channel, 'copyright', $this->copyright);
        $this->appendElement($dom, $channel, 'lastBuildDate', $this->formatDate(new DateTimeImmutable()));
        $this->appendElement($dom, $channel, 'ttl', (string)$this->ttl);
        $this->appendElement($dom, $channel, 'atom:link', '', [
            'href' => $this->link . '/rss.xml',
            'rel' => 'self',
            'type' => 'application/rss+xml',
        ]);

        if ($this->imageUrl) {
            $img = $dom->createElement('image');
            $this->appendElement($dom, $img, 'url', $this->imageUrl);
            $this->appendElement($dom, $img, 'title', $this->title);
            $this->appendElement($dom, $img, 'link', $this->link);
            $channel->appendChild($img);
        }

        // Items
        foreach ($this->items as $item) {
            $itemEl = $dom->createElement('item');
            $this->appendElement($dom, $itemEl, 'title', $item['title']);
            $this->appendElement($dom, $itemEl, 'link', $item['link']);
            $guid = $dom->createElement('guid', $item['guid'] ?: $item['link']);
            $guid->setAttribute('isPermaLink', $item['guid'] ? 'false' : 'true');
            $itemEl->appendChild($guid);
            $this->appendElement($dom, $itemEl, 'description', $item['description'], [], true); // CDATA
            $this->appendElement($dom, $itemEl, 'pubDate', $this->formatDate($item['pubDate'] ?? new DateTimeImmutable()));
            foreach ($item['categories'] as $cat) {
                $this->appendElement($dom, $itemEl, 'category', $cat);
            }
            if ($item['author']) $this->appendElement($dom, $itemEl, 'author', $item['author']);
            if ($item['enclosure']) {
                $enc = $dom->createElement('enclosure');
                $enc->setAttribute('url', $item['enclosure'][0]);
                $enc->setAttribute('type', $item['enclosure'][1]);
                $enc->setAttribute('length', (string)$item['enclosure'][2]);
                $itemEl->appendChild($enc);
            }
            $channel->appendChild($itemEl);
        }

        return $dom->saveXML();
    }

    private function appendElement(DOMDocument $dom, DOMElement $parent, string $name, string $value, array $attrs = [], bool $cdata = false): void
    {
        if ($value === '') return;
        $el = $dom->createElement($name);
        if ($cdata) {
            $el->appendChild($dom->createCDATASection($value));
        } else {
            $el->textContent = $value;
        }
        foreach ($attrs as $k => $v) $el->setAttribute($k, $v);
        $parent->appendChild($el);
    }

    private function formatDate(DateTimeImmutable|string $date): string
    {
        if (is_string($date)) $date = new DateTimeImmutable($date);
        return $date->format('r'); // RFC 2822 格式
    }

    // 輸出並設定 HTTP 快取頭
    public function output(): void
    {
        header('Content-Type: application/rss+xml; charset=UTF-8');
        header('Cache-Control: public, max-age=' . ($this->ttl * 60));
        header('Last-Modified: ' . gmdate('D, d M Y H:i:s', time()) . ' GMT');
        echo $this->render();
    }
}

控制器使用範例 (資料庫整合)

<?php
// rss.php
require 'vendor/autoload.php';
require 'RssFeed.php';

$pdo = new PDO('mysql:host=127.0.0.1;dbname=blog;charset=utf8mb4', 'user', 'pass');
$stmt = $pdo->query("SELECT id, title, slug, excerpt, content, published_at, category FROM posts WHERE status='published' ORDER BY published_at DESC LIMIT 50");

$feed = new RssFeed();
$feed->title('我的部落格')
     ->link('https://blog.example.com')
     ->description('分享技術與生活')
     ->language('zh-TW')
     ->ttl(60)
     ->addNamespace('content', 'https://purl.org/rss/1.0/modules/content/')
     ->addNamespace('atom', 'https://www.w3.org/2005/Atom');

while ($row = $stmt->fetch()) {
    $feed->addItem([
        'title' => $row['title'],
        'link' => "https://blog.example.com/post/{$row['slug']}",
        'guid' => "https://blog.example.com/post/{$row['slug']}",
        'description' => $row['excerpt'], // 或 $row['content'] 全文
        'pubDate' => new DateTimeImmutable($row['published_at']),
        'categories' => [$row['category']],
    ]);
}

$feed->output();

Podcast (iTunes) 擴充支援

$feed->addNamespace('itunes', 'https://www.itunes.com/dtds/podcast-1.0.dtd');

// Channel 級
$feed->addItem([]); // dummy 為了在 channel 加入 itunes 元素,實際建議擴充類別

// Item 級 (音訊單集)
$feed->addItem([
    'title' => 'EP01: PHP 8.2 新特性',
    'link' => 'https://podcast.example.com/ep01',
    'guid' => 'ep01',
    'description' => '本集介紹 readonly class, DNF types...',
    'pubDate' => new DateTimeImmutable('2024-01-01'),
    'enclosure' => [
        'https://cdn.example.com/ep01.mp3',
        'audio/mpeg',
        12345678, // bytes
    ],
    'categories' => ['Technology', 'Programming'],
]);
// 需在 render() 中補充 itunes:author, itunes:summary, itunes:duration, itunes:explicit 等

驗證與除錯工具

  • W3C Feed Validator:https://validator.w3.org/feed/ 輸入 URL 或上傳 XML。
  • Podcast 驗證:https://castfeedvalidator.com/ 專門檢查 iTunes 命名空間。
  • 瀏覽器檢視:直接開啟 RSS URL,現代瀏覽器會美化顯示 XML 樹狀結構。

效能優化:快取生成結果

// 簡單檔案快取
$cacheFile = __DIR__ . '/cache/rss.xml';
$ttl = 300; // 5 分鐘

if (file_exists($cacheFile) && (time() - filemtime($cacheFile) < $ttl)) {
    header('Content-Type: application/rss+xml; charset=UTF-8');
    readfile($cacheFile);
    exit;
}

$feed = new RssFeed();
// ... 建構 feed
$xml = $feed->render();
file_put_contents($cacheFile, $xml);
echo $xml;

RSS 雖為成熟技術,但正確實作需注意「XML 編碼、CDATA 使用、RFC 2822 日期、命名空間宣告、HTTP 快取頭」。封裝可復用的 RssFeed 類別,能讓專案輕鬆支援多種訂閱格式 (RSS 2.0, Atom, JSON Feed, Podcast)。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved