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

PHP 實時匯率轉換:從 Yahoo Finance API 遷移到現代免費方案(含完整代碼與容錯)

     編輯:PHP 相關知識

⚠️ 重要提醒:Yahoo Finance 官方 API 已停止服務

原文使用 download.finance.yahoo.com/d/quotes.csv 接口,**該端點已於 2017 年底永久關閉**。直接運行舊代碼會返回 404 或空數據。本文提供三個**現役、免費、無需 API Key** 的替代方案,並封裝生產級客戶端。

方案對照:現役免費匯率 API

API端點示例頻率限制數據源備註
Frankfurter(推薦)https://api.frankfurter.dev/v1/latest?from=USD&to=CNY無嚴格限制(公共實例)歐洲央行 ECB開源、支援歷史數據、HTTPS、JSON
ExchangeRate-APIhttps://open.er-api.com/v6/latest/USD1,500 次/月(免費)多源聚合需註冊獲取 Key,免費額度較大
CurrencyLayerhttps://api.currencylayer.com/live?access_key=KEY¤cies=CNY250 次/月(免費)商業級免費版僅 HTTP、不支援 HTTPS、基礎貨幣鎖定 USD

生產級匯率客戶端實現(以 Frankfurter 為例)

<?php
/**
 * 現代匯率服務:支援多提供商降級、緩存、異常處理、PSR-18 兼容
 */
declare(strict_types=1);

namespace App\Service;

use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestFactoryInterface;
use Psr\Cache\CacheItemPoolInterface;
use Psr\Log\LoggerInterface;

final class ExchangeRateService
{
    private const CACHE_TTL = 3600; // 1 小時緩存,匯率日級更新足夠
    private const BASE_URL = 'https://api.frankfurter.dev/v1/latest';
    
    public function __construct(
        private ClientInterface $httpClient,
        private RequestFactoryInterface $requestFactory,
        private CacheItemPoolInterface $cache,
        private LoggerInterface $logger
    ) {}
    
    /**
     * 獲取匯率
     * @param string $from 基礎貨幣(ISO 4217,如 USD、CNY)
     * @param string $to 目標貨幣
     * @return float 匯率(1 $from = ? $to)
     * @throws \RuntimeException 獲取失敗
     */
    public function getRate(string $from, string $to): float
    {
        $from = strtoupper($from);
        $to   = strtoupper($to);
        $key  = "fx_rate_{$from}_{$to}";
        
        // 1. 嘗試緩存
        $item = $this->cache->getItem($key);
        if ($item->isHit()) {
            return (float) $item->get();
        }
        
        // 2. 請求 API
        $url = self::BASE_URL . "?from={$from}&to={$to}";
        $request = $this->requestFactory->createRequest('GET', $url);
        $request = $request->withHeader('Accept', 'application/json');
        $request = $request->withHeader('User-Agent', 'MyApp/1.0 (+https://example.com)');
        
        try {
            $response = $this->httpClient->sendRequest($request);
            $status = $response->getStatusCode();
            $body = (string) $response->getBody();
            
            if ($status !== 200) {
                throw new \RuntimeException("HTTP $status: $body");
            }
            
            $data = json_decode($body, true, 512, JSON_THROW_ON_ERROR);
            
            if (!isset($data['rates'][$to])) {
                throw new \RuntimeException("貨幣 $to 不存在於響應中");
            }
            
            $rate = (float) $data['rates'][$to];
            
            // 3. 寫入緩存
            $item->set($rate);
            $item->expiresAfter(self::CACHE_TTL);
            $this->cache->save($item);
            
            return $rate;
            
        } catch (\Throwable $e) {
            $this->logger->error('匯率獲取失敗', [
                'from' => $from, 'to' => $to, 'error' => $e->getMessage()
            ]);
            // 4. 降級:嘗試讀取過期緩存(stale-while-revalidate)
            if ($this->cache->hasItem($key)) {
                $stale = $this->cache->getItem($key);
                if (!$stale->isHit()) return (float) $stale->get(); // 即使過期也返回
            }
            throw new \RuntimeException("匯率服務暫時不可用", 0, $e);
        }
    }
    
    /** 批量轉換 */
    public function convert(float $amount, string $from, string $to): float
    {
        return round($amount * $this->getRate($from, $to), 4);
    }
}

簡易版(無框架依賴,適合快速接入)

<?php
/**
 * 簡易匯率獲取:cURL + 文件緩存 + 多源降級
 */
function getExchangeRate(string $from, string $to): float
{
    $from = strtoupper($from);
    $to   = strtoupper($to);
    $cacheFile = sys_get_temp_dir() . "/fx_{$from}_{$to}.json";
    $ttl = 3600;
    
    // 1. 讀緩存
    if (file_exists($cacheFile) && (time() - filemtime($cacheFile) < $ttl)) {
        $data = json_decode(file_get_contents($cacheFile), true);
        if (isset($data['rate'])) return (float) $data['rate'];
    }
    
    // 2. 嘗試主源 Frankfurter
    $rate = fetchFrankfurter($from, $to);
    
    // 3. 降級備源 ExchangeRate-API (需註冊 KEY)
    if ($rate === null) {
        $rate = fetchExchangeRateApi($from, $to);
    }
    
    // 4. 最後嘗試讀取過期緩存
    if ($rate === null && file_exists($cacheFile)) {
        $data = json_decode(file_get_contents($cacheFile), true);
        $rate = $data['rate'] ?? null;
    }
    
    if ($rate === null) {
        throw new RuntimeException("所有匯率源均不可用");
    }
    
    // 5. 寫緩存
    file_put_contents($cacheFile, json_encode(['rate' => $rate, 'time' => time()]));
    return $rate;
}

function fetchFrankfurter(string $from, string $to): ?float
{
    $url = "https://api.frankfurter.dev/v1/latest?from={$from}&to={$to}";
    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT => 5,
        CURLOPT_SSL_VERIFYPEER => true,
        CURLOPT_USERAGENT => 'MyApp/1.0',
        CURLOPT_HTTPHEADER => ['Accept: application/json'],
    ]);
    $resp = curl_exec($ch);
    $err = curl_error($ch);
    $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);
    
    if ($err || $code !== 200) return null;
    $data = json_decode($resp, true);
    return $data['rates'][$to] ?? null;
}

function fetchExchangeRateApi(string $from, string $to): ?float
{
    $key = 'YOUR_FREE_KEY'; // 到 open.er-api.com 註冊
    $url = "https://open.er-api.com/v6/latest/{$from}?apikey={$key}";
    // ... 同樣 cURL 邏輯,解析 $data['rates'][$to]
    return null; // 占位
}

// 使用示例
try {
    $rate = getExchangeRate('CNY', 'USD');
    echo "1 CNY = $rate USD\n";
    echo "100 CNY = " . round(100 * $rate, 2) . " USD\n";
} catch (Throwable $e) {
    echo "錯誤: " . $e->getMessage();
}

關鍵工程化細節

  • 緩存策略: 匯率日間波動小,緩存 1-6 小時極大降低外部依賴風險;採用 stale-while-revalidate(過期仍返回舊值,異步更新)保證高可用。
  • 浮點精度: 匯率運算建議用 bcmath 或 brick/math 任意精度庫,避免浮點誤差;金融級系統請用 int 存儲「分」或最小單位。
  • 貨幣代碼規範: 嚴格使用 ISO 4217 三字母代碼(大寫),入參先 strtoupper 並白名單校驗。
  • HTTPS & 驗證: 生產環境強制 CURLOPT_SSL_VERIFYPEER => true,禁止關閉驗證。
  • 監控告警: 記錄 API 延遲、錯誤率、緩存命中率;連續失敗觸發告警切換備源。

歷史代碼遷移對照

舊代碼(Yahoo)現代替代
download.finance.yahoo.com/d/quotes.csvhttps://api.frankfurter.dev/v1/latest
CSV 解析 explode(',', $raw)JSON 解析 json_decode($resp, true)
無緩存、每請求必呼 APIRedis/文件緩存 + TTL + 降級
無異常處理、靜默失敗分層異常 + 結構化日志 + 告警
硬編碼 User-Agent 偽裝 IE誠實 UA:MyApp/1.0 (+https://example.com)

結語

匯率轉換看似簡單,實則是「外部依賴治理」的縮影:**選擇可持續維護的開源/免費源、建立多級緩存與降級、嚴格異常處理與可觀測性**。將舊 Yahoo 代碼替換為現代客戶端,不僅修復可用性,更為系統穩定性鋪路。

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