universal-dev-standards 6.14.0-beta.3 → 6.14.0-beta.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/uds.js +7 -1
- package/bundled/ai/standards/ai-response-navigation.ai.yaml +43 -3
- package/bundled/ai/standards/checkin-standards.ai.yaml +25 -6
- package/bundled/ai/standards/full-coverage-testing.ai.yaml +46 -5
- package/bundled/ai/standards/pipeline-security-gates.ai.yaml +5 -1
- package/bundled/core/ai-response-navigation.md +128 -12
- package/bundled/core/full-coverage-testing.md +57 -3
- package/bundled/extensions/frameworks/fat-free-patterns.md +937 -0
- package/bundled/extensions/languages/csharp-style.md +464 -0
- package/bundled/extensions/languages/php-style.md +700 -0
- package/bundled/extensions/locales/zh-cn.md +717 -0
- package/bundled/extensions/locales/zh-tw.md +717 -0
- package/bundled/locales/COVERAGE.md +5 -4
- package/bundled/locales/zh-CN/CHANGELOG.md +54 -3
- package/bundled/locales/zh-CN/README.md +2 -2
- package/bundled/locales/zh-CN/SECURITY.md +1 -1
- package/bundled/locales/zh-CN/core/ai-response-navigation.md +110 -12
- package/bundled/locales/zh-CN/core/full-coverage-testing.md +61 -7
- package/bundled/locales/zh-CN/docs/CHEATSHEET.md +3 -1
- package/bundled/locales/zh-CN/docs/FEATURE-REFERENCE.md +8 -5
- package/bundled/locales/zh-CN/skills/README.md +1 -0
- package/bundled/locales/zh-CN/skills/comprehension-ladder/SKILL.md +289 -0
- package/bundled/locales/zh-CN/skills/comprehension-ladder/eval-cases.md +261 -0
- package/bundled/locales/zh-TW/CHANGELOG.md +54 -3
- package/bundled/locales/zh-TW/README.md +2 -2
- package/bundled/locales/zh-TW/SECURITY.md +1 -1
- package/bundled/locales/zh-TW/core/ai-response-navigation.md +110 -12
- package/bundled/locales/zh-TW/core/full-coverage-testing.md +61 -7
- package/bundled/locales/zh-TW/docs/CHEATSHEET.md +3 -1
- package/bundled/locales/zh-TW/docs/FEATURE-REFERENCE.md +8 -5
- package/bundled/locales/zh-TW/skills/README.md +1 -0
- package/bundled/locales/zh-TW/skills/comprehension-ladder/SKILL.md +289 -0
- package/bundled/locales/zh-TW/skills/comprehension-ladder/eval-cases.md +261 -0
- package/bundled/skills/README.md +1 -0
- package/bundled/skills/comprehension-ladder/SKILL.md +283 -0
- package/bundled/skills/comprehension-ladder/eval-cases.md +255 -0
- package/bundled/templates/gates/check-anti-fake-tests.mjs +991 -0
- package/bundled/templates/gates/check-stubs.mjs +644 -0
- package/package.json +3 -3
- package/src/commands/audit.js +11 -0
- package/src/commands/check.js +124 -24
- package/src/commands/init.js +45 -9
- package/src/commands/update.js +183 -21
- package/src/core/install-records.js +2 -1
- package/src/i18n/messages.js +50 -9
- package/src/installers/standards-installer.js +16 -23
- package/src/reconciler/backup-manager.js +418 -82
- package/src/reconciler/index.js +27 -5
- package/src/reconciler/install-roots.js +90 -0
- package/src/reconciler/plan-executor.js +33 -13
- package/src/uninstallers/hook-uninstaller.js +7 -4
- package/src/utils/command-hash-ownership.js +103 -0
- package/src/utils/copier.js +78 -1
- package/src/utils/gate-scripts.js +141 -0
- package/src/utils/git-hooks.js +8 -4
- package/src/utils/health-scorer.js +10 -7
- package/src/utils/locale.js +19 -0
- package/src/utils/skill-hash-ownership.js +64 -0
- package/src/utils/skills-installer.js +12 -1
- package/src/utils/test-change-check.js +160 -0
- package/src/utils/test-policy.js +214 -0
- package/src/utils/update-summary.js +29 -0
- package/standards-registry.json +21 -7
|
@@ -0,0 +1,937 @@
|
|
|
1
|
+
# Fat-Free Framework Development Patterns
|
|
2
|
+
# Fat-Free Framework 開發模式
|
|
3
|
+
|
|
4
|
+
**Version**: 1.0.0
|
|
5
|
+
**Last Updated**: 2025-12-23
|
|
6
|
+
**Applicability**: All Fat-Free Framework (F3) v3.8+ projects
|
|
7
|
+
**適用範圍**: 所有 Fat-Free Framework (F3) v3.8+ 專案
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Purpose | 目的
|
|
12
|
+
|
|
13
|
+
This guide defines development patterns and best practices for Fat-Free Framework (F3) projects to ensure consistent, maintainable, and secure applications.
|
|
14
|
+
|
|
15
|
+
本指南定義 Fat-Free Framework (F3) 專案的開發模式與最佳實踐,確保應用程式的一致性、可維護性與安全性。
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Project Structure | 專案結構
|
|
20
|
+
|
|
21
|
+
### Recommended Directory Layout | 建議目錄結構
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
project-root/
|
|
25
|
+
├── app/
|
|
26
|
+
│ ├── Controllers/ # 控制器
|
|
27
|
+
│ │ ├── BaseController.php
|
|
28
|
+
│ │ ├── Api/
|
|
29
|
+
│ │ │ └── UserController.php
|
|
30
|
+
│ │ └── Web/
|
|
31
|
+
│ │ └── HomeController.php
|
|
32
|
+
│ ├── Models/ # 資料模型
|
|
33
|
+
│ │ ├── User.php
|
|
34
|
+
│ │ └── Traits/
|
|
35
|
+
│ │ └── HasTimestamps.php
|
|
36
|
+
│ ├── Services/ # 業務邏輯層
|
|
37
|
+
│ │ └── UserService.php
|
|
38
|
+
│ ├── Repositories/ # 資料存取層
|
|
39
|
+
│ │ └── UserRepository.php
|
|
40
|
+
│ └── Middleware/ # 中介軟體
|
|
41
|
+
│ ├── AuthMiddleware.php
|
|
42
|
+
│ └── CorsMiddleware.php
|
|
43
|
+
├── config/
|
|
44
|
+
│ ├── config.ini # 主設定檔
|
|
45
|
+
│ ├── config.dev.ini # 開發環境
|
|
46
|
+
│ ├── config.staging.ini # 測試環境
|
|
47
|
+
│ └── config.prod.ini # 生產環境
|
|
48
|
+
├── public/
|
|
49
|
+
│ ├── index.php # 入口點
|
|
50
|
+
│ └── assets/
|
|
51
|
+
├── storage/
|
|
52
|
+
│ ├── cache/
|
|
53
|
+
│ ├── logs/
|
|
54
|
+
│ └── tmp/
|
|
55
|
+
├── templates/ # 視圖模板
|
|
56
|
+
│ ├── layouts/
|
|
57
|
+
│ │ └── main.html
|
|
58
|
+
│ └── pages/
|
|
59
|
+
│ └── home.html
|
|
60
|
+
├── tests/
|
|
61
|
+
│ ├── Unit/
|
|
62
|
+
│ └── Integration/
|
|
63
|
+
├── vendor/
|
|
64
|
+
├── composer.json
|
|
65
|
+
└── .env
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## Routing | 路由定義
|
|
71
|
+
|
|
72
|
+
### RESTful Routes | RESTful 路由
|
|
73
|
+
|
|
74
|
+
```php
|
|
75
|
+
// ✅ 正確:RESTful 路由定義
|
|
76
|
+
// config/routes.ini 或 app/routes.php
|
|
77
|
+
|
|
78
|
+
// API Routes | API 路由
|
|
79
|
+
$f3->route('GET /api/users', 'App\Controllers\Api\UserController->index');
|
|
80
|
+
$f3->route('GET /api/users/@id', 'App\Controllers\Api\UserController->show');
|
|
81
|
+
$f3->route('POST /api/users', 'App\Controllers\Api\UserController->store');
|
|
82
|
+
$f3->route('PUT /api/users/@id', 'App\Controllers\Api\UserController->update');
|
|
83
|
+
$f3->route('DELETE /api/users/@id', 'App\Controllers\Api\UserController->destroy');
|
|
84
|
+
|
|
85
|
+
// Web Routes | 網頁路由
|
|
86
|
+
$f3->route('GET /', 'App\Controllers\Web\HomeController->index');
|
|
87
|
+
$f3->route('GET /login', 'App\Controllers\Web\AuthController->showLogin');
|
|
88
|
+
$f3->route('POST /login', 'App\Controllers\Web\AuthController->login');
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Route Groups | 路由群組
|
|
92
|
+
|
|
93
|
+
```php
|
|
94
|
+
// ✅ 正確:使用路由群組
|
|
95
|
+
class RouteGroup
|
|
96
|
+
{
|
|
97
|
+
public static function register(Base $f3): void
|
|
98
|
+
{
|
|
99
|
+
// API v1 群組
|
|
100
|
+
self::apiV1Routes($f3);
|
|
101
|
+
|
|
102
|
+
// Admin 群組
|
|
103
|
+
self::adminRoutes($f3);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
private static function apiV1Routes(Base $f3): void
|
|
107
|
+
{
|
|
108
|
+
$prefix = '/api/v1';
|
|
109
|
+
|
|
110
|
+
$f3->route("GET {$prefix}/users", 'Api\V1\UserController->index');
|
|
111
|
+
$f3->route("GET {$prefix}/users/@id", 'Api\V1\UserController->show');
|
|
112
|
+
$f3->route("POST {$prefix}/users", 'Api\V1\UserController->store');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
private static function adminRoutes(Base $f3): void
|
|
116
|
+
{
|
|
117
|
+
$prefix = '/admin';
|
|
118
|
+
|
|
119
|
+
// 套用認證中介軟體
|
|
120
|
+
$f3->route("GET {$prefix}/dashboard [auth]", 'Admin\DashboardController->index');
|
|
121
|
+
$f3->route("GET {$prefix}/users [auth,admin]", 'Admin\UserController->index');
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Route Parameters | 路由參數
|
|
127
|
+
|
|
128
|
+
```php
|
|
129
|
+
// ✅ 正確:路由參數處理
|
|
130
|
+
$f3->route('GET /users/@id', function(Base $f3, array $params) {
|
|
131
|
+
$userId = (int) $params['id'];
|
|
132
|
+
// ...
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// ✅ 正確:可選參數
|
|
136
|
+
$f3->route('GET /posts/@category/@page', 'PostController->list');
|
|
137
|
+
$f3->route('GET /posts/@category', 'PostController->list'); // page 可選
|
|
138
|
+
|
|
139
|
+
// ✅ 正確:正則表達式約束
|
|
140
|
+
$f3->route('GET /users/@id:[0-9]+', 'UserController->show'); // 僅數字
|
|
141
|
+
$f3->route('GET /posts/@slug:[a-z0-9-]+', 'PostController->show'); // slug 格式
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## Controllers | 控制器
|
|
147
|
+
|
|
148
|
+
### Base Controller | 基礎控制器
|
|
149
|
+
|
|
150
|
+
```php
|
|
151
|
+
<?php
|
|
152
|
+
|
|
153
|
+
declare(strict_types=1);
|
|
154
|
+
|
|
155
|
+
namespace App\Controllers;
|
|
156
|
+
|
|
157
|
+
use Base;
|
|
158
|
+
use Template;
|
|
159
|
+
|
|
160
|
+
abstract class BaseController
|
|
161
|
+
{
|
|
162
|
+
protected Base $f3;
|
|
163
|
+
protected Template $template;
|
|
164
|
+
|
|
165
|
+
public function __construct()
|
|
166
|
+
{
|
|
167
|
+
$this->f3 = Base::instance();
|
|
168
|
+
$this->template = Template::instance();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* 渲染視圖
|
|
173
|
+
*/
|
|
174
|
+
protected function render(string $view, array $data = []): string
|
|
175
|
+
{
|
|
176
|
+
foreach ($data as $key => $value) {
|
|
177
|
+
$this->f3->set($key, $value);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return $this->template->render($view);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* 回傳 JSON 回應
|
|
185
|
+
*/
|
|
186
|
+
protected function json(mixed $data, int $statusCode = 200): void
|
|
187
|
+
{
|
|
188
|
+
$this->f3->set('RESPONSE.status', $statusCode);
|
|
189
|
+
header('Content-Type: application/json; charset=utf-8');
|
|
190
|
+
echo json_encode($data, JSON_UNESCAPED_UNICODE);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* 重新導向
|
|
195
|
+
*/
|
|
196
|
+
protected function redirect(string $path): void
|
|
197
|
+
{
|
|
198
|
+
$this->f3->reroute($path);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* 取得 POST/PUT 資料
|
|
203
|
+
*/
|
|
204
|
+
protected function getBody(): array
|
|
205
|
+
{
|
|
206
|
+
$contentType = $this->f3->get('HEADERS.Content-Type') ?? '';
|
|
207
|
+
|
|
208
|
+
if (str_contains($contentType, 'application/json')) {
|
|
209
|
+
return json_decode($this->f3->get('BODY'), true) ?? [];
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return $this->f3->get('POST') ?? [];
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### API Controller | API 控制器
|
|
218
|
+
|
|
219
|
+
```php
|
|
220
|
+
<?php
|
|
221
|
+
|
|
222
|
+
declare(strict_types=1);
|
|
223
|
+
|
|
224
|
+
namespace App\Controllers\Api;
|
|
225
|
+
|
|
226
|
+
use App\Controllers\BaseController;
|
|
227
|
+
use App\Services\UserService;
|
|
228
|
+
use Base;
|
|
229
|
+
|
|
230
|
+
class UserController extends BaseController
|
|
231
|
+
{
|
|
232
|
+
private UserService $userService;
|
|
233
|
+
|
|
234
|
+
public function __construct()
|
|
235
|
+
{
|
|
236
|
+
parent::__construct();
|
|
237
|
+
$this->userService = new UserService();
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* GET /api/users
|
|
242
|
+
*/
|
|
243
|
+
public function index(Base $f3): void
|
|
244
|
+
{
|
|
245
|
+
$page = (int) ($f3->get('GET.page') ?? 1);
|
|
246
|
+
$perPage = (int) ($f3->get('GET.per_page') ?? 20);
|
|
247
|
+
|
|
248
|
+
$result = $this->userService->paginate($page, $perPage);
|
|
249
|
+
|
|
250
|
+
$this->json([
|
|
251
|
+
'data' => $result['data'],
|
|
252
|
+
'meta' => [
|
|
253
|
+
'current_page' => $page,
|
|
254
|
+
'per_page' => $perPage,
|
|
255
|
+
'total' => $result['total'],
|
|
256
|
+
],
|
|
257
|
+
]);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* GET /api/users/@id
|
|
262
|
+
*/
|
|
263
|
+
public function show(Base $f3, array $params): void
|
|
264
|
+
{
|
|
265
|
+
$userId = (int) $params['id'];
|
|
266
|
+
|
|
267
|
+
try {
|
|
268
|
+
$user = $this->userService->findById($userId);
|
|
269
|
+
$this->json(['data' => $user]);
|
|
270
|
+
} catch (UserNotFoundException $e) {
|
|
271
|
+
$this->json(['error' => 'User not found'], 404);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* POST /api/users
|
|
277
|
+
*/
|
|
278
|
+
public function store(Base $f3): void
|
|
279
|
+
{
|
|
280
|
+
$data = $this->getBody();
|
|
281
|
+
|
|
282
|
+
// 驗證輸入
|
|
283
|
+
$errors = $this->validate($data, [
|
|
284
|
+
'email' => 'required|email',
|
|
285
|
+
'name' => 'required|min:2',
|
|
286
|
+
'password' => 'required|min:8',
|
|
287
|
+
]);
|
|
288
|
+
|
|
289
|
+
if (!empty($errors)) {
|
|
290
|
+
$this->json(['errors' => $errors], 422);
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
$user = $this->userService->create($data);
|
|
295
|
+
$this->json(['data' => $user], 201);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
---
|
|
301
|
+
|
|
302
|
+
## Models | 資料模型
|
|
303
|
+
|
|
304
|
+
### Using DB Mapper | 使用 DB Mapper
|
|
305
|
+
|
|
306
|
+
```php
|
|
307
|
+
<?php
|
|
308
|
+
|
|
309
|
+
declare(strict_types=1);
|
|
310
|
+
|
|
311
|
+
namespace App\Models;
|
|
312
|
+
|
|
313
|
+
use DB\SQL\Mapper;
|
|
314
|
+
use DateTime;
|
|
315
|
+
|
|
316
|
+
class User extends Mapper
|
|
317
|
+
{
|
|
318
|
+
public function __construct()
|
|
319
|
+
{
|
|
320
|
+
parent::__construct(
|
|
321
|
+
\Base::instance()->get('DB'),
|
|
322
|
+
'users'
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* 根據 email 查詢使用者
|
|
328
|
+
*/
|
|
329
|
+
public function findByEmail(string $email): ?self
|
|
330
|
+
{
|
|
331
|
+
$this->load(['email = ?', $email]);
|
|
332
|
+
|
|
333
|
+
return $this->dry() ? null : $this;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* 驗證密碼
|
|
338
|
+
*/
|
|
339
|
+
public function verifyPassword(string $password): bool
|
|
340
|
+
{
|
|
341
|
+
return password_verify($password, $this->password);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* 設定密碼(自動雜湊)
|
|
346
|
+
*/
|
|
347
|
+
public function setPassword(string $password): void
|
|
348
|
+
{
|
|
349
|
+
$this->password = password_hash($password, PASSWORD_ARGON2ID);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* 轉換為陣列
|
|
354
|
+
*/
|
|
355
|
+
public function toArray(): array
|
|
356
|
+
{
|
|
357
|
+
return [
|
|
358
|
+
'id' => $this->id,
|
|
359
|
+
'email' => $this->email,
|
|
360
|
+
'name' => $this->name,
|
|
361
|
+
'created_at' => $this->created_at,
|
|
362
|
+
'updated_at' => $this->updated_at,
|
|
363
|
+
];
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* 儲存前的 hook
|
|
368
|
+
*/
|
|
369
|
+
public function beforeSave(): void
|
|
370
|
+
{
|
|
371
|
+
$now = (new DateTime())->format('Y-m-d H:i:s');
|
|
372
|
+
|
|
373
|
+
if ($this->dry()) {
|
|
374
|
+
$this->created_at = $now;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
$this->updated_at = $now;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
### Query Best Practices | 查詢最佳實踐
|
|
383
|
+
|
|
384
|
+
```php
|
|
385
|
+
// ✅ 正確:使用參數化查詢
|
|
386
|
+
$user = new User();
|
|
387
|
+
$user->load(['email = ? AND status = ?', $email, 'active']);
|
|
388
|
+
|
|
389
|
+
// ✅ 正確:使用具名參數
|
|
390
|
+
$user->load([
|
|
391
|
+
'email = :email AND status = :status',
|
|
392
|
+
':email' => $email,
|
|
393
|
+
':status' => 'active',
|
|
394
|
+
]);
|
|
395
|
+
|
|
396
|
+
// ✅ 正確:複雜查詢
|
|
397
|
+
$users = $user->find(
|
|
398
|
+
['department_id = ? AND role IN ?', $deptId, ['admin', 'manager']],
|
|
399
|
+
[
|
|
400
|
+
'order' => 'created_at DESC',
|
|
401
|
+
'limit' => 20,
|
|
402
|
+
'offset' => 0,
|
|
403
|
+
]
|
|
404
|
+
);
|
|
405
|
+
|
|
406
|
+
// ❌ 危險:字串拼接(SQL Injection)
|
|
407
|
+
$user->load("email = '$email'"); // 禁止!
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
---
|
|
411
|
+
|
|
412
|
+
## Middleware | 中介軟體
|
|
413
|
+
|
|
414
|
+
### Authentication Middleware | 認證中介軟體
|
|
415
|
+
|
|
416
|
+
```php
|
|
417
|
+
<?php
|
|
418
|
+
|
|
419
|
+
declare(strict_types=1);
|
|
420
|
+
|
|
421
|
+
namespace App\Middleware;
|
|
422
|
+
|
|
423
|
+
use Base;
|
|
424
|
+
|
|
425
|
+
class AuthMiddleware
|
|
426
|
+
{
|
|
427
|
+
/**
|
|
428
|
+
* 驗證 JWT Token
|
|
429
|
+
*/
|
|
430
|
+
public function beforeRoute(Base $f3): void
|
|
431
|
+
{
|
|
432
|
+
$authHeader = $f3->get('HEADERS.Authorization') ?? '';
|
|
433
|
+
|
|
434
|
+
if (!str_starts_with($authHeader, 'Bearer ')) {
|
|
435
|
+
$this->unauthorized($f3, 'Missing authorization header');
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
$token = substr($authHeader, 7);
|
|
440
|
+
|
|
441
|
+
try {
|
|
442
|
+
$payload = $this->validateToken($token);
|
|
443
|
+
$f3->set('AUTH.user_id', $payload['user_id']);
|
|
444
|
+
$f3->set('AUTH.role', $payload['role']);
|
|
445
|
+
} catch (InvalidTokenException $e) {
|
|
446
|
+
$this->unauthorized($f3, 'Invalid or expired token');
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
private function unauthorized(Base $f3, string $message): void
|
|
451
|
+
{
|
|
452
|
+
$f3->set('RESPONSE.status', 401);
|
|
453
|
+
header('Content-Type: application/json');
|
|
454
|
+
echo json_encode(['error' => $message]);
|
|
455
|
+
exit;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
private function validateToken(string $token): array
|
|
459
|
+
{
|
|
460
|
+
// JWT 驗證邏輯
|
|
461
|
+
// ...
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
### CORS Middleware | CORS 中介軟體
|
|
467
|
+
|
|
468
|
+
```php
|
|
469
|
+
<?php
|
|
470
|
+
|
|
471
|
+
declare(strict_types=1);
|
|
472
|
+
|
|
473
|
+
namespace App\Middleware;
|
|
474
|
+
|
|
475
|
+
use Base;
|
|
476
|
+
|
|
477
|
+
class CorsMiddleware
|
|
478
|
+
{
|
|
479
|
+
private array $allowedOrigins = [
|
|
480
|
+
'https://example.com',
|
|
481
|
+
'https://app.example.com',
|
|
482
|
+
];
|
|
483
|
+
|
|
484
|
+
public function beforeRoute(Base $f3): void
|
|
485
|
+
{
|
|
486
|
+
$origin = $f3->get('HEADERS.Origin') ?? '';
|
|
487
|
+
|
|
488
|
+
if (in_array($origin, $this->allowedOrigins, true)) {
|
|
489
|
+
header("Access-Control-Allow-Origin: {$origin}");
|
|
490
|
+
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
|
|
491
|
+
header('Access-Control-Allow-Headers: Content-Type, Authorization');
|
|
492
|
+
header('Access-Control-Max-Age: 86400');
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// Preflight request
|
|
496
|
+
if ($f3->get('VERB') === 'OPTIONS') {
|
|
497
|
+
$f3->set('RESPONSE.status', 204);
|
|
498
|
+
exit;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
### Registering Middleware | 註冊中介軟體
|
|
505
|
+
|
|
506
|
+
```php
|
|
507
|
+
// public/index.php 或 bootstrap.php
|
|
508
|
+
|
|
509
|
+
$f3 = Base::instance();
|
|
510
|
+
|
|
511
|
+
// 全域中介軟體
|
|
512
|
+
$f3->set('MIDDLEWARE', [
|
|
513
|
+
'before' => [
|
|
514
|
+
new \App\Middleware\CorsMiddleware(),
|
|
515
|
+
],
|
|
516
|
+
]);
|
|
517
|
+
|
|
518
|
+
// 路由專用中介軟體
|
|
519
|
+
$f3->route('GET /api/users [auth]', 'UserController->index');
|
|
520
|
+
$f3->route('GET /admin/* [auth,admin]', 'AdminController->*');
|
|
521
|
+
|
|
522
|
+
// 註冊中介軟體別名
|
|
523
|
+
$f3->set('middleware.auth', new \App\Middleware\AuthMiddleware());
|
|
524
|
+
$f3->set('middleware.admin', new \App\Middleware\AdminMiddleware());
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
---
|
|
528
|
+
|
|
529
|
+
## Configuration | 設定管理
|
|
530
|
+
|
|
531
|
+
### Main Configuration | 主設定檔
|
|
532
|
+
|
|
533
|
+
```ini
|
|
534
|
+
; config/config.ini
|
|
535
|
+
|
|
536
|
+
[globals]
|
|
537
|
+
; 應用程式設定
|
|
538
|
+
APP_NAME = "My Application"
|
|
539
|
+
APP_ENV = production
|
|
540
|
+
APP_DEBUG = false
|
|
541
|
+
APP_URL = "https://example.com"
|
|
542
|
+
|
|
543
|
+
; 時區
|
|
544
|
+
TZ = Asia/Taipei
|
|
545
|
+
|
|
546
|
+
; 資料庫設定
|
|
547
|
+
DB.dsn = "mysql:host=localhost;dbname=myapp;charset=utf8mb4"
|
|
548
|
+
DB.user = "root"
|
|
549
|
+
DB.password = ""
|
|
550
|
+
|
|
551
|
+
; 快取設定
|
|
552
|
+
CACHE = "redis=127.0.0.1:6379"
|
|
553
|
+
|
|
554
|
+
; Session 設定
|
|
555
|
+
SESSION.name = "my_app_session"
|
|
556
|
+
SESSION.expire = 3600
|
|
557
|
+
|
|
558
|
+
; 日誌設定
|
|
559
|
+
LOG.level = "error"
|
|
560
|
+
LOG.path = "storage/logs/"
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
### Environment-Specific Configuration | 環境專用設定
|
|
564
|
+
|
|
565
|
+
```ini
|
|
566
|
+
; config/config.dev.ini
|
|
567
|
+
|
|
568
|
+
[globals]
|
|
569
|
+
APP_ENV = development
|
|
570
|
+
APP_DEBUG = true
|
|
571
|
+
|
|
572
|
+
DB.dsn = "mysql:host=localhost;dbname=myapp_dev;charset=utf8mb4"
|
|
573
|
+
DB.user = "dev_user"
|
|
574
|
+
DB.password = "dev_password"
|
|
575
|
+
|
|
576
|
+
CACHE = "folder=storage/cache/"
|
|
577
|
+
|
|
578
|
+
LOG.level = "debug"
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
```ini
|
|
582
|
+
; config/config.prod.ini
|
|
583
|
+
|
|
584
|
+
[globals]
|
|
585
|
+
APP_ENV = production
|
|
586
|
+
APP_DEBUG = false
|
|
587
|
+
|
|
588
|
+
; 從環境變數讀取敏感資訊
|
|
589
|
+
DB.dsn = "${DATABASE_URL}"
|
|
590
|
+
DB.user = "${DB_USERNAME}"
|
|
591
|
+
DB.password = "${DB_PASSWORD}"
|
|
592
|
+
|
|
593
|
+
CACHE = "redis=${REDIS_URL}"
|
|
594
|
+
|
|
595
|
+
LOG.level = "error"
|
|
596
|
+
```
|
|
597
|
+
|
|
598
|
+
### Loading Configuration | 載入設定
|
|
599
|
+
|
|
600
|
+
```php
|
|
601
|
+
<?php
|
|
602
|
+
// public/index.php
|
|
603
|
+
|
|
604
|
+
$f3 = Base::instance();
|
|
605
|
+
|
|
606
|
+
// 載入主設定
|
|
607
|
+
$f3->config('config/config.ini');
|
|
608
|
+
|
|
609
|
+
// 根據環境載入對應設定
|
|
610
|
+
$env = getenv('APP_ENV') ?: 'production';
|
|
611
|
+
$envConfig = "config/config.{$env}.ini";
|
|
612
|
+
|
|
613
|
+
if (file_exists($envConfig)) {
|
|
614
|
+
$f3->config($envConfig);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// 從 .env 載入敏感資訊(開發環境)
|
|
618
|
+
if (file_exists('.env') && $env === 'development') {
|
|
619
|
+
$f3->config('.env');
|
|
620
|
+
}
|
|
621
|
+
```
|
|
622
|
+
|
|
623
|
+
### Environment Variables | 環境變數處理
|
|
624
|
+
|
|
625
|
+
```php
|
|
626
|
+
// ✅ 正確:安全地讀取環境變數
|
|
627
|
+
class Config
|
|
628
|
+
{
|
|
629
|
+
public static function get(string $key, mixed $default = null): mixed
|
|
630
|
+
{
|
|
631
|
+
$f3 = Base::instance();
|
|
632
|
+
|
|
633
|
+
// 優先從 F3 設定讀取
|
|
634
|
+
$value = $f3->get($key);
|
|
635
|
+
|
|
636
|
+
if ($value !== null) {
|
|
637
|
+
return $value;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// 次之從環境變數讀取
|
|
641
|
+
$envValue = getenv(str_replace('.', '_', strtoupper($key)));
|
|
642
|
+
|
|
643
|
+
return $envValue !== false ? $envValue : $default;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
public static function isDebug(): bool
|
|
647
|
+
{
|
|
648
|
+
return (bool) self::get('APP_DEBUG', false);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
public static function isProduction(): bool
|
|
652
|
+
{
|
|
653
|
+
return self::get('APP_ENV') === 'production';
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
```
|
|
657
|
+
|
|
658
|
+
---
|
|
659
|
+
|
|
660
|
+
## Error Handling | 錯誤處理
|
|
661
|
+
|
|
662
|
+
### Custom Error Handler | 自訂錯誤處理器
|
|
663
|
+
|
|
664
|
+
```php
|
|
665
|
+
<?php
|
|
666
|
+
|
|
667
|
+
$f3 = Base::instance();
|
|
668
|
+
|
|
669
|
+
// 設定錯誤處理器
|
|
670
|
+
$f3->set('ONERROR', function(Base $f3) {
|
|
671
|
+
$error = $f3->get('ERROR');
|
|
672
|
+
|
|
673
|
+
// 記錄錯誤
|
|
674
|
+
error_log(sprintf(
|
|
675
|
+
"[%s] %s: %s in %s:%d\nTrace: %s",
|
|
676
|
+
date('Y-m-d H:i:s'),
|
|
677
|
+
$error['code'],
|
|
678
|
+
$error['text'],
|
|
679
|
+
$error['trace'][0]['file'] ?? 'unknown',
|
|
680
|
+
$error['trace'][0]['line'] ?? 0,
|
|
681
|
+
json_encode($error['trace'])
|
|
682
|
+
));
|
|
683
|
+
|
|
684
|
+
// API 回應
|
|
685
|
+
if (str_starts_with($f3->get('PATH'), '/api/')) {
|
|
686
|
+
header('Content-Type: application/json');
|
|
687
|
+
echo json_encode([
|
|
688
|
+
'error' => [
|
|
689
|
+
'code' => $error['code'],
|
|
690
|
+
'message' => $f3->get('APP_DEBUG')
|
|
691
|
+
? $error['text']
|
|
692
|
+
: 'An error occurred',
|
|
693
|
+
],
|
|
694
|
+
]);
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// 網頁回應
|
|
699
|
+
$template = Template::instance();
|
|
700
|
+
echo $template->render('errors/' . $error['code'] . '.html');
|
|
701
|
+
});
|
|
702
|
+
```
|
|
703
|
+
|
|
704
|
+
### Exception Handling | 例外處理
|
|
705
|
+
|
|
706
|
+
```php
|
|
707
|
+
// ✅ 正確:在控制器中處理例外
|
|
708
|
+
class UserController extends BaseController
|
|
709
|
+
{
|
|
710
|
+
public function show(Base $f3, array $params): void
|
|
711
|
+
{
|
|
712
|
+
try {
|
|
713
|
+
$user = $this->userService->findById((int) $params['id']);
|
|
714
|
+
$this->json(['data' => $user->toArray()]);
|
|
715
|
+
|
|
716
|
+
} catch (UserNotFoundException $e) {
|
|
717
|
+
$this->json(['error' => 'User not found'], 404);
|
|
718
|
+
|
|
719
|
+
} catch (DatabaseException $e) {
|
|
720
|
+
$this->f3->error(500, 'Database error');
|
|
721
|
+
|
|
722
|
+
} catch (Throwable $e) {
|
|
723
|
+
// 記錄未預期的錯誤
|
|
724
|
+
error_log($e->getMessage() . "\n" . $e->getTraceAsString());
|
|
725
|
+
$this->f3->error(500, 'Internal server error');
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
```
|
|
730
|
+
|
|
731
|
+
---
|
|
732
|
+
|
|
733
|
+
## Testing | 測試
|
|
734
|
+
|
|
735
|
+
### Unit Test Example | 單元測試範例
|
|
736
|
+
|
|
737
|
+
```php
|
|
738
|
+
<?php
|
|
739
|
+
|
|
740
|
+
declare(strict_types=1);
|
|
741
|
+
|
|
742
|
+
namespace Tests\Unit;
|
|
743
|
+
|
|
744
|
+
use App\Services\UserService;
|
|
745
|
+
use App\Repositories\UserRepository;
|
|
746
|
+
use PHPUnit\Framework\TestCase;
|
|
747
|
+
|
|
748
|
+
class UserServiceTest extends TestCase
|
|
749
|
+
{
|
|
750
|
+
private UserService $userService;
|
|
751
|
+
private UserRepository $userRepository;
|
|
752
|
+
|
|
753
|
+
protected function setUp(): void
|
|
754
|
+
{
|
|
755
|
+
$this->userRepository = $this->createMock(UserRepository::class);
|
|
756
|
+
$this->userService = new UserService($this->userRepository);
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
public function testFindByIdReturnsUser(): void
|
|
760
|
+
{
|
|
761
|
+
// Arrange
|
|
762
|
+
$expectedUser = ['id' => 1, 'email' => 'test@example.com'];
|
|
763
|
+
$this->userRepository
|
|
764
|
+
->method('findById')
|
|
765
|
+
->with(1)
|
|
766
|
+
->willReturn($expectedUser);
|
|
767
|
+
|
|
768
|
+
// Act
|
|
769
|
+
$result = $this->userService->findById(1);
|
|
770
|
+
|
|
771
|
+
// Assert
|
|
772
|
+
$this->assertEquals($expectedUser, $result);
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
public function testFindByIdThrowsWhenNotFound(): void
|
|
776
|
+
{
|
|
777
|
+
// Arrange
|
|
778
|
+
$this->userRepository
|
|
779
|
+
->method('findById')
|
|
780
|
+
->with(999)
|
|
781
|
+
->willReturn(null);
|
|
782
|
+
|
|
783
|
+
// Assert
|
|
784
|
+
$this->expectException(UserNotFoundException::class);
|
|
785
|
+
|
|
786
|
+
// Act
|
|
787
|
+
$this->userService->findById(999);
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
```
|
|
791
|
+
|
|
792
|
+
### Integration Test Example | 整合測試範例
|
|
793
|
+
|
|
794
|
+
```php
|
|
795
|
+
<?php
|
|
796
|
+
|
|
797
|
+
declare(strict_types=1);
|
|
798
|
+
|
|
799
|
+
namespace Tests\Integration;
|
|
800
|
+
|
|
801
|
+
use Base;
|
|
802
|
+
use PHPUnit\Framework\TestCase;
|
|
803
|
+
|
|
804
|
+
class UserApiTest extends TestCase
|
|
805
|
+
{
|
|
806
|
+
private Base $f3;
|
|
807
|
+
|
|
808
|
+
protected function setUp(): void
|
|
809
|
+
{
|
|
810
|
+
$this->f3 = Base::instance();
|
|
811
|
+
$this->f3->config('config/config.test.ini');
|
|
812
|
+
|
|
813
|
+
// 設定測試資料庫
|
|
814
|
+
$this->setupTestDatabase();
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
protected function tearDown(): void
|
|
818
|
+
{
|
|
819
|
+
$this->cleanupTestDatabase();
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
public function testGetUsersReturnsJsonList(): void
|
|
823
|
+
{
|
|
824
|
+
// Arrange
|
|
825
|
+
$this->insertTestUsers(3);
|
|
826
|
+
|
|
827
|
+
// Act
|
|
828
|
+
$this->f3->mock('GET /api/users');
|
|
829
|
+
|
|
830
|
+
// Assert
|
|
831
|
+
$response = json_decode($this->f3->get('RESPONSE'), true);
|
|
832
|
+
$this->assertCount(3, $response['data']);
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
public function testCreateUserReturns201(): void
|
|
836
|
+
{
|
|
837
|
+
// Arrange
|
|
838
|
+
$userData = [
|
|
839
|
+
'email' => 'new@example.com',
|
|
840
|
+
'name' => 'New User',
|
|
841
|
+
'password' => 'SecureP@ss123',
|
|
842
|
+
];
|
|
843
|
+
|
|
844
|
+
// Act
|
|
845
|
+
$this->f3->mock('POST /api/users', $userData);
|
|
846
|
+
|
|
847
|
+
// Assert
|
|
848
|
+
$this->assertEquals(201, $this->f3->get('RESPONSE.status'));
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
```
|
|
852
|
+
|
|
853
|
+
---
|
|
854
|
+
|
|
855
|
+
## Security Checklist | 安全檢查清單
|
|
856
|
+
|
|
857
|
+
### Before Deployment | 部署前檢查
|
|
858
|
+
|
|
859
|
+
- [ ] **APP_DEBUG = false** in production
|
|
860
|
+
- [ ] Database credentials in environment variables (not in code)
|
|
861
|
+
- [ ] All user inputs validated and sanitized
|
|
862
|
+
- [ ] Prepared statements used for all database queries
|
|
863
|
+
- [ ] XSS prevention (htmlspecialchars) applied to all output
|
|
864
|
+
- [ ] CSRF tokens implemented for forms
|
|
865
|
+
- [ ] Rate limiting configured for API endpoints
|
|
866
|
+
- [ ] HTTPS enforced
|
|
867
|
+
- [ ] Secure session configuration
|
|
868
|
+
- [ ] Error details hidden from users in production
|
|
869
|
+
|
|
870
|
+
---
|
|
871
|
+
|
|
872
|
+
## Related Standards | 相關標準
|
|
873
|
+
|
|
874
|
+
- [PHP Coding Style Guide](../languages/php-style.md) - PHP 編碼風格指南
|
|
875
|
+
- [Anti-Hallucination Standard](../../core/anti-hallucination.md) - AI 協作防幻覺標準
|
|
876
|
+
- [Code Check-in Standards](../../core/checkin-standards.md) - 程式碼簽入檢查點標準
|
|
877
|
+
- [Testing Standards](../../core/testing-standards.md) - 測試標準
|
|
878
|
+
|
|
879
|
+
---
|
|
880
|
+
|
|
881
|
+
## Quick Reference Card | 快速參考卡
|
|
882
|
+
|
|
883
|
+
```
|
|
884
|
+
┌─────────────────────────────────────────────────────────┐
|
|
885
|
+
│ Fat-Free Framework Patterns │
|
|
886
|
+
├─────────────────────────────────────────────────────────┤
|
|
887
|
+
│ Project Structure │
|
|
888
|
+
├─────────────────────────────────────────────────────────┤
|
|
889
|
+
│ app/Controllers/ │ 控制器 │
|
|
890
|
+
│ app/Models/ │ 資料模型 (DB Mapper) │
|
|
891
|
+
│ app/Services/ │ 業務邏輯 │
|
|
892
|
+
│ app/Middleware/ │ 中介軟體 │
|
|
893
|
+
│ config/ │ 設定檔 │
|
|
894
|
+
│ templates/ │ 視圖模板 │
|
|
895
|
+
├─────────────────────────────────────────────────────────┤
|
|
896
|
+
│ Routing │
|
|
897
|
+
├─────────────────────────────────────────────────────────┤
|
|
898
|
+
│ GET /users │ $f3->route('GET /users', ...) │
|
|
899
|
+
│ Route params │ /users/@id:[0-9]+ │
|
|
900
|
+
│ Middleware │ [auth,admin] │
|
|
901
|
+
├─────────────────────────────────────────────────────────┤
|
|
902
|
+
│ Configuration │
|
|
903
|
+
├─────────────────────────────────────────────────────────┤
|
|
904
|
+
│ Main config │ config/config.ini │
|
|
905
|
+
│ Dev config │ config/config.dev.ini │
|
|
906
|
+
│ Prod config │ config/config.prod.ini │
|
|
907
|
+
│ Env vars │ ${DATABASE_URL} │
|
|
908
|
+
├─────────────────────────────────────────────────────────┤
|
|
909
|
+
│ Security Essentials │
|
|
910
|
+
├─────────────────────────────────────────────────────────┤
|
|
911
|
+
│ ✅ Prepared statements for all queries │
|
|
912
|
+
│ ✅ Input validation before processing │
|
|
913
|
+
│ ✅ APP_DEBUG = false in production │
|
|
914
|
+
│ ✅ Sensitive config in environment variables │
|
|
915
|
+
└─────────────────────────────────────────────────────────┘
|
|
916
|
+
```
|
|
917
|
+
|
|
918
|
+
---
|
|
919
|
+
|
|
920
|
+
## Version History | 版本歷史
|
|
921
|
+
|
|
922
|
+
| Version | Date | Changes |
|
|
923
|
+
|---------|------|---------|
|
|
924
|
+
| 1.0.0 | 2025-12-23 | Initial Fat-Free Framework v3.8+ patterns |
|
|
925
|
+
|
|
926
|
+
---
|
|
927
|
+
|
|
928
|
+
## License | 授權
|
|
929
|
+
|
|
930
|
+
This standard is released under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/).
|
|
931
|
+
|
|
932
|
+
本標準以 [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) 授權發布。
|
|
933
|
+
|
|
934
|
+
---
|
|
935
|
+
|
|
936
|
+
**Maintainer**: Development Team
|
|
937
|
+
**維護者**: 開發團隊
|