從文件名/路徑中可靠提取擴展名 (不含點號),處理邊界情況:多點文件名、隱藏文件、無擴展名、大小寫、URL 路徑等。
<?php
function getExtensionPathinfo(string $filename): ?string {
$info = pathinfo($filename);
return $info['extension'] ?? null;
}
// 測試
var_dump(getExtensionPathinfo('image.jpg')); // string(3) "jpg"
var_dump(getExtensionPathinfo('archive.tar.gz')); // string(2) "gz" (只取最後一段)
var_dump(getExtensionPathinfo('.htaccess')); // string(8) "htaccess" (隱藏文件視為無目錄名)
var_dump(getExtensionPathinfo('README')); // NULL
var_dump(getExtensionPathinfo('/path/to/file.PDF')); // string(3) "PDF" (保持原大小寫)
var_dump(getExtensionPathinfo('file.name.with.dots.txt')); // string(3) "txt"
var_dump(getExtensionPathinfo('https://cdn.example.com/avatar.png?version=123')); // string(3) "png" (自動忽略查詢字符串)
<?php
function getExtensionSpl(string $filename): string {
return (new SplFileInfo($filename))->getExtension();
}
// 行為與 pathinfo() 一致,但更適合現代 OOP 代碼風格
// 可鏈式調用:(new SplFileInfo($path))->getExtension()
<?php
function getExtensionManual(string $filename): ?string {
$basename = basename($filename); // 去除目錄
$pos = strrpos($basename, '.'); // 最後一個點
if ($pos === false || $pos === 0) return null; // 無點 或 以點開頭(.htaccess)
return substr($basename, $pos + 1);
}
// 注意:不自動處理 URL 查詢字符串,需額外 parse_url()
<?php
function getExtensionRegex(string $filename): ?string {
// 匹配最後一個點後面的非點字符直到結尾或問號/井號
if (preg_match('/\.([^.\/?#]+)(?:[\?#]|$)/', $filename, $m)) {
return $m[1];
}
return null;
}
| 輸入 | pathinfo() | SplFileInfo | 手動解析 | 備註 |
|---|---|---|---|---|
| `photo.jpg` | `jpg` | `jpg` | `jpg` | 標準 |
| `archive.tar.gz` | `gz` | `gz` | `gz` | 雙擴展名只取最後 |
| `.env` | `env` | `env` | `null` | 隱藏文件視為擴展名 |
| `README` | `null` | `''` | `null` | 無擴展名 |
| `file.` | `''` | `''` | `''` | 以點結尾,空擴展名 |
| `/path/to/file.PDF` | `PDF` | `PDF` | `PDF` | 保持大小寫 |
| `url.com/a.png?v=1` | `png` | `png` | `null` | 需配合 parse_url() |
| `file.name.with.dots.txt` | `txt` | `txt` | `txt` | 多點文件名 |
<?php
/**
* 獲取標準化文件擴展名 (小寫、無點號)
*
* @param string $filename 文件名或路徑或 URL
* @param bool $stripQuery 是否移除 URL 查詢字符串
* @return string|null 擴展名小寫,無擴展名返回 null
*/
function getFileExtension(string $filename, bool $stripQuery = true): ?string {
// 1. 處理 URL
if ($stripQuery && (str_starts_with($filename, 'https://') || str_starts_with($filename, 'https://'))) {
$parsed = parse_url($filename);
$filename = $parsed['path'] ?? $filename;
}
// 2. 核心提取 (pathinfo 最可靠)
$ext = pathinfo($filename, PATHINFO_EXTENSION);
// 3. 規範化
if ($ext === '' || $ext === null) return null;
return strtolower($ext);
}
/**
* 驗證擴展名是否在允許白名單中
*/
function isAllowedExtension(string $filename, array $allowed, bool $strict = true): bool {
$ext = getFileExtension($filename);
if ($ext === null) return false;
$allowed = array_map('strtolower', $allowed);
return in_array($ext, $allowed, $strict);
}
// 使用示例
$allowed = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'pdf'];
$files = ['photo.JPG', 'document.pdf', 'script.php', 'image.png?version=2', '.htaccess'];
foreach ($files as $file) {
$ext = getFileExtension($file);
$ok = isAllowedExtension($file, $allowed);
echo "{$file} => ext: " . ($ext ?? 'none') . " | allowed: " . ($ok ? 'yes' : 'no') . "\n";
}
// 輸出:
// photo.JPG => ext: jpg | allowed: yes
// document.pdf => ext: pdf | allowed: yes
// script.php => ext: php | allowed: no
// image.png?version=2 => ext: png | allowed: yes
// .htaccess => ext: htaccess | allowed: no
<?php
function validateUploadedFile(array $file, array $allowedExt, array $allowedMime): bool {
// 1. 檢查上傳錯誤
if ($file['error'] !== UPLOAD_ERR_OK) return false;
// 2. 檢查 MIME 類型 (finfo, 服務端檢測)
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($file['tmp_name']);
if (!in_array($mime, $allowedMime, true)) return false;
// 3. 檢查擴展名與 MIME 一致性
$ext = getFileExtension($file['name']);
$expectedMime = mimeFromExtension($ext); // 需維護映射表
if ($mime !== $expectedMime) return false; // 防止 .php 改名為 .jpg
// 4. 檢查白名單
return isAllowedExtension($file['name'], $allowedExt);
}
// MIME 映射表示例
function mimeFromExtension(string $ext): string {
$map = [
'jpg' => 'image/jpeg', 'jpeg' => 'image/jpeg',
'png' => 'image/png', 'gif' => 'image/gif',
'webp' => 'image/webp', 'pdf' => 'application/pdf',
];
return $map[strtolower($ext)] ?? 'application/octet-stream';
}
<?php
$files = ['test.jpg', 'archive.tar.gz', '.env', 'README', 'path/to/file.PDF'];
$iterations = 1_000_000;
$start = hrtime(true);
for ($i = 0; $i < $iterations; $i++) {
foreach ($files as $f) pathinfo($f, PATHINFO_EXTENSION);
}
echo "pathinfo: " . (hrtime(true) - $start) / 1e9 . "s\n"; // ~0.18s
$start = hrtime(true);
for ($i = 0; $i < $iterations; $i++) {
foreach ($files as $f) (new SplFileInfo($f))->getExtension();
}
echo "SplFileInfo: " . (hrtime(true) - $start) / 1e9 . "s\n"; // ~0.45s
$start = hrtime(true);
for ($i = 0; $i < $iterations; $i++) {
foreach ($files as $f) {
$b = basename($f);
$p = strrpos($b, '.');
if ($p !== false && $p > 0) substr($b, $p + 1);
}
}
echo "Manual: " . (hrtime(true) - $start) / 1e9 . "s\n"; // ~0.12s (最快但功能最少)