ThinkPHP 模板引擎預設不支持變量直接運算(如 {$a+$b}),官方推薦通過自定義函數實現。本文提供完整封裝方案,支援四則運算、比較邏輯、格式化輸出等常見場景。
ThinkPHP 模板語法 {:functionName($arg1, $arg2)} 可調用任意 PHP 可調用單元(函數、類靜態方法、匿名函數)。利用此機制封裝運算邏輯。
// application/common.php (ThinkPHP 5/6 自動載入)
/**
* 模板運算函數庫
* 用法:{:math_add($a, $b)} {:math_sub($price, $discount)} {:math_mul($qty, $unit_price)}
*/
/** 加法 */
function math_add($a, $b): float|int {
return (float)$a + (float)$b;
}
/** 減法 */
function math_sub($a, $b): float|int {
return (float)$a - (float)$b;
}
/** 乘法 */
function math_mul($a, $b): float|int {
return (float)$a * (float)$b;
}
/** 除法(含除零保護) */
function math_div($a, $b, int $precision = 2): float|string {
$b = (float)$b;
if ($b == 0) return 'N/A'; // 或拋異常、返回 0、返回 null
return round((float)$a / $b, $precision);
}
/** 取模 */
function math_mod($a, $b): int {
return (int)$a % (int)$b;
}
/** 百分比計算 */
function math_pct($part, $total, int $precision = 2): string {
$total = (float)$total;
if ($total == 0) return '0%';
return round((float)$part / $total * 100, $precision) . '%';
}
/*** 比較運算(返回布爾,配合模板 if 標簽使用) **/
function math_gt($a, $b): bool { return (float)$a > (float)$b; }
function math_lt($a, $b): bool { return (float)$a < (float)$b; }
function math_gte($a, $b): bool { return (float)$a >= (float)$b; }
function math_lte($a, $b): bool { return (float)$a <= (float)$b; }
function math_eq($a, $b): bool { return (float)$a == (float)$b; }
function math_neq($a, $b): bool { return (float)$a != (float)$b; }
/** 條件選擇(三元運算符替代) */
function math_if($condition, $trueVal, $falseVal) {
return $condition ? $trueVal : $falseVal;
}
/** 數字格式化 */
function math_format($number, int $decimals = 2, string $decPoint = '.', string $thousandsSep = ','): string {
return number_format((float)$number, $decimals, $decPoint, $thousandsSep);
}
/** 貨幣格式化 */
function math_currency($amount, string $symbol = '¥', int $decimals = 2): string {
return $symbol . math_format($amount, $decimals);
}
/** 範圍限制 */
function math_clamp($value, $min, $max): float {
$value = (float)$value;
return max($min, min($max, $value));
}
{* 基礎四則運算 *}
總價:{:math_mul($goods.price, $goods.num)}
折扣價:{:math_sub($goods.price, $goods.discount)}
單價:{:math_div($order.total, $order.qty, 2)}
{* 比較邏輯配合 if *}
{if condition="math_gt($user.score, 1000)"}
VIP 用戶
{else}
普通用戶
{/if}
{* 條件顯示 *}
庫存:{:math_if(math_gt($goods.stock, 0), '有貨', '缺貨')}
{* 格式化輸出 *}
金額:{:math_currency($order.amount)}
佔比:{:math_pct($cate.sales, $total_sales)}
{* 陣列元素運算(注意語法) *}
{* 錯誤:{:math_add($data['a'], $data['b'])} *}
{* 正確:{:math_add($data.a, $data.b)} 或 {:math_add($data.a, $data.b)} *}
總計:{:math_add($order.subtotal, $order.shipping)}
// app/common/TemplateMath.php
namespace app\common;
class TemplateMath {
/** 加法 */
public static function add($a, $b): float { return (float)$a + (float)$b; }
/** 減法 */
public static function sub($a, $b): float { return (float)$a - (float)$b; }
/** 乘法 */
public static function mul($a, $b): float { return (float)$a * (float)$b; }
/** 除法 */
public static function div($a, $b, int $precision = 2): float|string {
$b = (float)$b;
return $b == 0 ? 'N/A' : round((float)$a / $b, $precision);
}
/** 格式化 */
public static function format($num, int $d = 2): string { return number_format((float)$num, $d); }
/** 貨幣 */
public static function currency($amt, string $sym = '¥'): string { return $sym . self::format($amt); }
}
// 模板調用:{:app\common\TemplateMath::add($a, $b)}
// 或註冊別名(config/template.php 'function_alias' => ['tmath' => 'app\common\TemplateMath'])
// 然後:{:tmath::add($a, $b)}
// config/provider.php
return [
app\common\TemplateMath::class => [],
];
// 控制器/模板中自動可用:{:app('tmath')->add($a, $b)}
// 註冊模板標簽(ThinkPHP 6+)
\think\facade\View::addFunction('add', 'app\common\TemplateMath::add');
\think\facade\View::addFunction('sub', 'app\common\TemplateMath::sub');
// 然後模板可直接:{$a|add=$b} 或 {:add($a, $b)}
// app/viewmodel/OrderViewModel.php
class OrderViewModel {
protected Order $order;
public function __construct(Order $order) { $this->order = $order; }
public function getTotalPrice(): float {
return $this->order->subtotal + $this->order->shipping - $this->order->discount;
}
public function getFormattedTotal(): string {
return '¥' . number_format($this->getTotalPrice(), 2);
}
public function isFreeShipping(): bool {
return $this->order->subtotal >= 99;
}
}
// 控制器
public function detail($id) {
$order = Order::find($id);
$viewModel = new OrderViewModel($order);
return view('order/detail', ['vm' => $viewModel]);
}
// 模板:純屬性訪問,零邏輯
總價:{$vm.formattedTotal}
{$vm.isFreeShipping ? '免運費' : '需付運費'}
| 方案 | 優點 | 缺點 | 適用場景 |
|---|---|---|---|
| 全局函數 | 簡單直接、模板調用短 | 污染全局命名空間、難測試、無依賴注入 | 極簡項目、快速原型 |
| 靜態類方法 | 命名空間隔離、可測試、IDE 提示友好 | 靜態調用難 Mock、模板語法稍長 | 中小型項目、團隊協作 |
| 服務容器/視圖模型 | 依賴注入、可測試、關注點分離、複雜邏輯封裝 | 初始代碼量大、學習曲線陡 | 中大型項目、長期維護、團隊規範 |
| 模板原生擴展 | 語法最優雅(修飾符管道)、性能最好 | 框架版本綁定、調試相對困難 | 高性能要求、框架深度用戶 |
{:htmlspecialchars(math_add($a, $b))} 或使用框架內置輸出轉義bcmath 擴展或整數分單位,避免 0.1 + 0.2 = 0.30000000000000004$data.key 而非 $data['key'],嵌套用 $data.key.subThinkPHP 模板不支持直接運算是為了強制「邏輯與視圖分離」。正確路徑:簡單運算用靜態類/全局函數,複雜邏輯用視圖模型/服務類,極致性能用模板原生擴展。選型原則:能在控制器/視圖模型預計算的,絕不放到模板運算。