🌐 Ngôn ngữ: 🇻🇳 Tiếng Việt (hiện tại) · 🇬🇧 English · 🇯🇵 日本語
Hướng dẫn tạo API đúng chuẩn trên GP247
Giới thiệu
Tài liệu này hướng dẫn cách tự tạo (viết mới) một API endpoint đúng chuẩn GP247 — dành cho lập trình viên muốn mở rộng site GP247 bằng các API riêng (ví dụ: xuất dữ liệu cho app di động, cho phần mềm quản lý bên ngoài, hay tích hợp bên thứ ba). Đọc xong, bạn sẽ biết đặt code ở đâu, khai báo route thế nào, dùng controller/middleware/xác thực ra sao, và có một ví dụ hoàn chỉnh copy vào là chạy được. Nếu bạn chỉ muốn gọi các API có sẵn (không tự viết), hãy đọc Giới thiệu API GP247 trước.
ℹ️ Quy tắc vàng của GP247: không sửa code lõi (
vendor/gp247/*). Muốn thêm API riêng cho một site cụ thể, hãy làm trong plugin (thư mụcapp/GP247/Plugins/) hoặc bằng cơ chế override quaapp/GP247/. Chỉ khi bạn là người bảo trì chính của góicore/front/shopmới thêm trực tiếp vào gói đó.
1. Nắm kiến trúc API trước khi viết
API của GP247 được ráp lại từ vài mảnh có sẵn. Hiểu chúng giúp bạn cắm code vào đúng chỗ.
1.1. Hai nhóm và tiền tố (prefix)
Mọi endpoint mới đặt dưới tiền tố v2:
| Nhóm | Hằng số dùng trong route | Giá trị | Dùng cho |
|---|---|---|---|
| Core (Admin) | GP247_API_V2_CORE_PREFIX |
api/v2/core |
Quản trị hệ thống (cần token admin) |
| Front (Storefront) | GP247_API_V2_FRONT_PREFIX |
api/v2/front |
Dữ liệu cửa hàng & tài khoản khách |
Hai hằng này khai báo cố định trong vendor/gp247/core/src/Library/Const.php. Luôn dùng hằng số
trong route (không viết chuỗi "api/v2/core" tay) để nếu sau này đổi tiền tố, code của bạn tự theo.
⚠️ REST API v1 đã đóng băng. Từ gp247/core, gp247/front, gp247/shop 3.3.0, API v1 (
GP247_API_CORE_PREFIX=api/core,GP247_API_FRONT_PREFIX=api/front) không nhận tính năng mới — chỉ còn bản vá bảo mật; site cài mới từ gp247/core 3.3.1 tắt sẵn v1. Endpoint v1 đang có vẫn chạy, nhưng đừng thêm endpoint mới vào đó. Xem API v1.
1.2. Bật chế độ API
API chỉ hoạt động khi bật cờ GP247_API_MODE. Kiểm tra trong file .env của site:
GP247_API_MODE=true
Nếu cờ này tắt, toàn bộ route API sẽ không được nạp (xem vendor/gp247/core/src/routes.php).
Khi cờ đã bật, chủ site còn bật/tắt từng nhóm trên màn quản lý API (lớp api.enabled, xem
Giới thiệu API, mục 5). Route đặt dưới api/v2/front theo công tắc
"v2 · API công khai & khách hàng (front)", dưới api/v2/core theo công tắc "v2 · API quản trị (core)" —
kể cả route do plugin thêm vào. Nhóm đang tắt trả 404 với code: api_disabled.
1.3. Cách route API được nạp tự động
Khi GP247_API_MODE=true, mỗi gói tự quét và nạp mọi file trong các thư mục sau, và bọc tất cả
trong nhóm middleware GP247_API_MIDDLEWARE:
vendor/gp247/core/src/Routes/Api/v2/*.phpvendor/gp247/shop/src/Routes/Api/v2/*.phpvendor/gp247/front/src/Routes/Api/v2/*.php
(File v1 nằm ở Routes/Api/*.php của cùng gói — chỉ để giữ endpoint cũ.)
GP247_API_MIDDLEWARE = ['api', 'api.request_id', 'json.response', 'api.enabled', 'api.extend'], trong đó api.extend là nhóm middleware GP247 định
nghĩa ở config('gp247-config.api.middleware'):
| Thứ tự | Middleware | Vai trò |
|---|---|---|
| 1 | json.response |
Ép mọi phản hồi trả về JSON (đặt header Accept: application/json). Cũng có trong GP247_API_MIDDLEWARE và chạy trước mọi middleware khác (kể cả auth:*), nên không mất dù config đã publish thiếu dòng này |
| 2 | api.connection |
Lớp khóa ứng dụng tùy chọn (apiconnection/apikey) — bật/tắt qua cấu hình |
| 3 | throttle:gp247-api |
Giới hạn tần suất, mặc định 1000 lần/phút mỗi IP (GP247_API_THROTTLE) |
⚠️ Thứ tự này cố định — không đổi. Đây là quy ước đã ghi rõ trong config.
ℹ️ Có từ: gp247/core 3.6.0 —
json.responsechạy trướcauth:*: thiếu token mà không gửiAccept: application/jsonvẫn nhận401JSON (trước đây500).👉 Hệ quả quan trọng cho plugin: route đặt trong
Routes/Api/v2/của core/shop/front sẽ tự động có 3 lớp trên. Nhưng route bạn thêm từ plugin lại nạp ở chỗ khác (mục 4) nên không tự có — bạn phải tự bọc chúng, hoặc (từ 3.3.0) đăng ký quaApiRouteRegistrar(mục 4.1).
ℹ️ Có từ: gp247/core, gp247/front, gp247/shop 3.3.0 — mỗi gói tự đăng ký route API của mình (kể cả thư mục
Routes/Api/v2/) quaGP247\Core\Api\Support\ApiRouteRegistrar; route của gói chỉ có khi gói đã được cài vào cơ sở dữ liệu. Đường dẫn và tên route không đổi.
1.4. Xác thực bằng Laravel Sanctum
GP247 dùng Sanctum (token dạng Bearer). Có hai "guard" (cổng đăng nhập) tách biệt:
| Guard | Provider | Dành cho | Cấp token ở đâu |
|---|---|---|---|
admin-api |
admin_provider |
Quản trị viên | api/v2/core/login |
customer-api |
customer_provider |
Khách/thành viên | api/v2/front/login |
Mỗi token còn mang phạm vi quyền (scope/ability) để giới hạn nó làm được gì. Các scope lấy từ
config('gp247-config.api.auth') (đổi được qua .env):
| Khóa config | Giá trị mặc định | Ý nghĩa |
|---|---|---|
api_scope_admin |
admin |
Admin thường |
api_scope_admin_supper |
admin-supper |
Admin cấp cao (vai trò administrator) |
api_scope_user |
user |
Thành viên đã kích hoạt |
api_scope_user_guest |
user-guest |
Thành viên chưa kích hoạt/khách |
Để bảo vệ một route cần đăng nhập, dùng cặp middleware:
'middleware' => [
'auth:admin-api', // or 'auth:customer-api' for front APIs
\GP247\Core\Api\Support\ApiAbilities::admin(), // or ApiAbilities::customer(): the allowed scopes
]
1.5. Helper gp247_namespace() — điểm cho phép override
Trong route GP247, controller không viết trực tiếp Controller::class mà bọc qua
gp247_namespace():
$catalog = gp247_namespace(\GP247\Shop\Api\V2\CatalogController::class);
Route::get('product/list', $catalog.'@productList');
Helper này tự kiểm tra: nếu tồn tại file override ở app/GP247/... thì dùng bản của app, ngược lại
dùng bản trong vendor. Nhờ đó người dùng cuối có thể override controller mà không sửa vendor.
Luôn dùng helper này khi trỏ route tới controller của một gói GP247\....
2. Quy ước đặt tên & định dạng phản hồi
Giữ đúng các quy ước sau để API mới đồng bộ với phần còn lại của hệ thống.
-
Gom theo tài nguyên: mỗi nhóm dữ liệu là một
Route::group(['prefix' => 'product'], ...). -
Hai action chuẩn:
list(danh sách) vàdetail/{id}(chi tiết một bản ghi). -
Tên method controller:
<tàiNguyên>Listvà<tàiNguyên>Detail(ví dụproductList,productDetail— như các controller v2 của gói shop). -
Luôn trả JSON qua helper
GP247\Core\Api\V2\ApiQuery(trả vềJsonResponse), hoặcresponse()->json(...)cho phản hồi không phải bản ghi. -
Thành công: bọc dữ liệu trong
data—{"data": …}cho một bản ghi (ApiQuery::one()), và{"data": [...], "meta": {...}, "links": {...}}cho danh sách (ApiQuery::page()). -
Thất bại/không tìm thấy: dùng
ApiErrorResponderđể có mã HTTP đúng nghĩa và thân lỗi chung{"error": 1, "msg": "…", "code": "…"}(bảng mã ở Giới thiệu API, mục 6).ApiQuery::one()tự trả404khi không có bản ghi:use GP247\Core\Api\Support\ApiErrorResponder; if (!$product) { return ApiErrorResponder::notFound(); // 404, code: not_found }ℹ️ Có từ: gp247/core 3.2.3 — lỗi xác thực, phân quyền, validate và giới hạn tần suất dưới
api/*được tự đổi sang dạng này; bạn chỉ cần tự trảnotFound()/forbidden()khi controller tự quyết định. Mẫu cũresponse()->json([...], 200)cho lỗi không còn dùng. -
Chỉ trả trường cần thiết: hàm
$toArraytruyền vàoApiQuerykhai báo đúng các trường trả về, thay vì trả nguyên model — cột mới thêm vào bảng sẽ không tự lộ ra API. Với dữ liệu công khai, lấy truy vấn gốc quaGP247\Core\Api\Support\ApiVisibility::visible(...)để API chỉ trả đúng những bản ghi website đang hiển thị. -
Phân trang, lọc, sắp xếp: dùng
ApiQuery::page($query, $request, $toArray, $filters, $sorts, $search)thay cho->get()khi trả danh sách. Client điều khiển bằngpage[number],page[size](mặc định 20, tối đa 100),filter[<tên>],sort=<tên>/sort=-<tên>vàq(tìm kiếm). Chỉ những tên lọc/sắp xếp bạn khai báo mới được nhận — tên lạ trả422(validation_failed), dữ liệu client không bao giờ thành tên cột trong truy vấn. Nội dung đa ngôn ngữ: lấy ngôn ngữ bằngApiQuery::lang($request)và các trường mô tả bằngApiQuery::described(...).
3. Ví dụ A — Thêm endpoint vào một gói bạn bảo trì (core/front/shop)
Áp dụng khi bạn là maintainer của gói (sửa trực tiếp trong vendor/gp247/<gói>), ví dụ thêm API
front công khai api/v2/front/review/list. Làm theo đúng thứ tự:
-
Tạo controller tại
vendor/gp247/<gói>/src/Api/V2/.... Với dữ liệu front, kế thừaRootFrontController; với dữ liệu admin, kế thừaRootAdminController.Tạo file
src/Api/V2/ReviewController.php:<?php namespace GP247\Shop\Api\V2; use GP247\Core\Api\V2\ApiQuery; use GP247\Front\Controllers\RootFrontController; use GP247\Shop\Models\ShopProductReview; use Illuminate\Database\Eloquent\Builder; use Illuminate\Http\Request; class ReviewController extends RootFrontController { /** * Get the public review list. * * @return \Illuminate\Http\JsonResponse */ public function reviewList(Request $request) { $query = ShopProductReview::query()->where('status', 1); return ApiQuery::page($query, $request, fn ($r) => self::review($r), ['product_id' => fn (Builder $q, string $v) => $q->where('product_id', $v)], // filter[product_id] ['created_at' => 'created_at']); // sort=created_at | -created_at } /** * Get one public review (404 when it does not exist). * * @return \Illuminate\Http\JsonResponse */ public function reviewDetail(Request $request, $id) { $review = ShopProductReview::query()->where('status', 1)->where('id', $id)->first(); return ApiQuery::one($review, fn ($r) => self::review($r)); } /** * Declared shape of a review: only these fields reach the API. */ private static function review($r): array { return [ 'id' => $r->id, 'product_id' => $r->product_id, 'point' => (int) $r->point, 'comment' => $r->comment, 'created_at' => optional($r->created_at)->toIso8601String(), ]; } } -
Tạo file route tại
src/Routes/Api/v2/review.php(chỉ cần đặt vào thư mụcRoutes/Api/v2/là gói tự nạp — không phải khai báo ở đâu khác):<?php use GP247\Shop\Api\V2\ReviewController; use Illuminate\Support\Facades\Route; Route::group([ 'prefix' => GP247_API_V2_FRONT_PREFIX, // => api/v2/front ], function () { $reviewController = gp247_namespace(ReviewController::class); Route::group(['prefix' => 'review'], function () use ($reviewController) { Route::get('list', $reviewController.'@reviewList'); Route::get('detail/{id}', $reviewController.'@reviewDetail'); }); }); -
Kiểm tra route đã nạp bằng lệnh Artisan (chạy ở thư mục gốc site):
php artisan route:list --path=api/v2/front/reviewNếu thành công, bạn thấy dòng
GET | api/v2/front/review/list. Nếu không thấy, kiểm tra lạiGP247_API_MODE=truetrong.envvà xóa cache route:php artisan route:clear. -
Gọi thử (endpoint công khai nên không cần token;
-gđể curl giữ nguyên dấu[ ]):curl -g -X GET "https://shop.example.com/api/v2/front/review/list?page[size]=10&sort=-created_at" -H "Accept: application/json"Kết quả mong đợi:
{ "data": [ { "id": 12, "product_id": 5, "point": 5, "comment": "…", "created_at": "2026-10-30T10:00:00+00:00" } ], "meta": { "current_page": 1, "per_page": 10, "total": 1, "last_page": 1 }, "links": { "first": "…", "last": "…", "prev": null, "next": null } }
Với endpoint admin (cần đăng nhập), đặt route trong
src/Routes/Api/v2/của gói và bọc thêm nhóm middlewareauth:admin-api+ApiAbilities::admin()vớiprefix => GP247_API_V2_CORE_PREFIX, mỗi route khai báoapi.admin:<tên route admin>(mục 5.1; xem mẫu ởvendor/gp247/shop/src/Routes/Api/v2/admin.php).
4. Ví dụ B — Thêm API từ một plugin (khuyến nghị cho site riêng)
Đây là cách đúng chuẩn khi bạn thêm API cho một site cụ thể mà không được sửa vendor. Plugin
đặt tại app/GP247/Plugins/<TênPlugin>/, và file Route.php của nó được core tự nạp.
⚠️ Khác biệt then chốt: route của plugin nạp ngoài nhóm middleware API (mục 1.3), nên bạn phải tự bọc
GP247_API_MIDDLEWAREvà tự đặtprefix. Nếu quên, API của bạn sẽ không trả JSON ép buộc, không qua lớpapi.connection, không theo công tắc bật/tắt API, và không có giới hạn tần suất.
-
Tạo controller trong plugin, ví dụ
app/GP247/Plugins/MyApi/Api/Front/StatusController.php:<?php namespace App\GP247\Plugins\MyApi\Api\Front; use GP247\Front\Controllers\RootFrontController; use Illuminate\Http\Request; class StatusController extends RootFrontController { /** * Return a simple health-check payload for external monitors. * * @return \Illuminate\Http\JsonResponse */ public function status(Request $request) { return response()->json(['data' => [ 'status' => 'ok', 'time' => gp247_time_now(), ]]); } } -
Khai báo route trong
app/GP247/Plugins/MyApi/Route.php, tự bọc nhóm middleware API:<?php use App\GP247\Plugins\MyApi\Api\Front\StatusController; use Illuminate\Support\Facades\Route; // Only register when the API mode is on, matching core behaviour. if (config('gp247-config.env.GP247_API_MODE')) { Route::group([ 'middleware' => GP247_API_MIDDLEWARE, // required: plugin routes load outside the API group 'prefix' => GP247_API_V2_FRONT_PREFIX, // => api/v2/front ], function () { $statusController = gp247_namespace(StatusController::class); Route::group(['prefix' => 'myapi'], function () use ($statusController) { Route::get('status', $statusController.'@status'); }); }); } -
Xóa cache và kiểm tra route:
php artisan route:clear php artisan route:list --path=api/v2/front/myapiNếu thành công, bạn thấy
GET | api/v2/front/myapi/status. -
Gọi thử:
curl -X GET "https://shop.example.com/api/v2/front/myapi/status" -H "Accept: application/json"Kết quả mong đợi:
{ "data": { "status": "ok", "time": "2026-07-30 10:00:00" } }
4.1. Cách gọn hơn: đăng ký qua ApiRouteRegistrar
ℹ️ Có từ: gp247/core 3.3.0
Thay vì tự bọc nhóm middleware trong Route.php, đặt route API vào một file riêng của plugin và đăng ký
nó từ Provider.php của plugin:
// app/GP247/Plugins/MyApi/Provider.php
\GP247\Core\Api\Support\ApiRouteRegistrar::register(__DIR__ . '/Routes/api.php');
// app/GP247/Plugins/MyApi/Routes/api.php — declare only the prefix and the routes
use Illuminate\Support\Facades\Route;
Route::group(['prefix' => GP247_API_V2_FRONT_PREFIX], function () {
$statusController = gp247_namespace(\App\GP247\Plugins\MyApi\Api\Front\StatusController::class);
Route::get('myapi/status', $statusController.'@status');
});
ApiRouteRegistrar tự bọc GP247_API_MIDDLEWARE (JSON, API Connection, giới hạn tần suất), không đăng ký gì
khi GP247_API_MODE tắt, và không đăng ký lại khi site đã route:cache. Hợp đồng lỗi {error, msg, code} áp
dụng cho mọi đường dẫn dưới api/core, api/front, api/v2/core, api/v2/front; route đặt dưới tiền tố riêng
(ngoài bốn tiền tố này) giữ thân lỗi mặc định của Laravel và chỉ theo công tắc tổng của màn API.
5. Tạo endpoint cần đăng nhập (bảo vệ bằng token)
Nếu API của bạn trả dữ liệu riêng tư (đơn hàng của khách, dữ liệu admin…), phải bọc bằng guard
Sanctum tương ứng. Trong controller, lấy người dùng đang đăng nhập qua $request->user().
Ví dụ route khách hàng cần đăng nhập (api.customer.active của gói shop từ chối token của tài khoản đã bị khoá
sau khi cấp token):
use GP247\Core\Api\Support\ApiAbilities;
Route::group([
'prefix' => GP247_API_V2_FRONT_PREFIX, // => api/v2/front
'middleware' => ['auth:customer-api', ApiAbilities::customer(), 'api.customer.active'],
], function () {
$ctrl = gp247_namespace(\App\GP247\Plugins\MyApi\Api\Front\WishlistController::class);
Route::get('member/wishlist/list', $ctrl.'@wishlistList');
});
Với route quản trị, xem mục 5.1.
Client phải gửi token đã lấy từ login trong header:
Authorization: Bearer <access_token>
Việc cấp token (
createToken) và thu hồi token (logout) đã có sẵn trong các controller đăng nhập của core/shop. Bạn chỉ cần tái dùng endpointapi/v2/core/login/api/v2/front/loginđể lấy token (v2 có thêmwith_refreshđể nhận refresh token,login/challengecho bước xác thực đăng nhập vàrefresh— xem Giới thiệu API, mục 2), không cần tự viết lại phần đăng nhập trừ khi có nhu cầu đặc biệt.
5.1. Endpoint quản trị: khai báo màn admin và giới hạn theo cửa hàng
ℹ️ Có từ: gp247/core 3.2.3
Token admin không tự cho phép xem mọi dữ liệu. Mỗi route dưới GP247_API_V2_CORE_PREFIX phải khai
báo màn hình admin tương ứng bằng middleware api.admin:<tên route admin>; GP247 chỉ cho gọi
khi tài khoản có quyền mở màn hình đó (cùng phân quyền với trang quản trị), áp rào chặn của plugin
đa cửa hàng và xác định cửa hàng của tài khoản:
Route::group([
'prefix' => GP247_API_V2_CORE_PREFIX, // => api/v2/core
'middleware' => [
'auth:admin-api',
\GP247\Core\Api\Support\ApiAbilities::admin(), // since 3.3.0
],
], function () {
$ctrl = gp247_namespace(\App\GP247\Plugins\MyApi\Api\Admin\ReportController::class);
Route::get('report/sales', $ctrl.'@salesList')
->middleware('api.admin:admin_report.sales'); // route name of the matching admin screen
});
Trong controller, luôn lọc truy vấn theo cửa hàng của tài khoản:
use GP247\Core\Api\Support\ApiStoreScope;
use GP247\Core\Api\V2\ApiQuery;
return ApiQuery::page(ApiStoreScope::apply(ShopOrder::query()), $request,
fn ($o) => ['id' => $o->id, 'total' => $o->total, 'status' => (int) $o->status]);
Gọi ApiStoreScope trên route thiếu api.admin sẽ báo lỗi (an toàn mặc định) — đây là cách GP247
nhắc bạn đã quên khai báo quyền.
5.2. Endpoint ghi quản trị, danh sách đồng bộ và sự kiện
ℹ️ Có từ: gp247/core 3.7.0
Phần này dành cho plugin muốn ERP hay công cụ đồng bộ ghi dữ liệu của plugin, đọc thay đổi theo updated_since và
nhận sự kiện của plugin — cùng cơ chế các endpoint có sẵn đang dùng (phía ứng dụng gọi thế nào:
Giới thiệu API — mục 6, 7).
a) Endpoint ghi quản trị. Khai báo api.admin:<tên route màn admin>,write: chỉ cho gọi khi tài khoản được thay đổi dữ
liệu trên màn đó — kiểm tra y như các thao tác của chính màn hình; tài khoản chỉ được xem nhận 403. Thêm hai middleware:
| Middleware | Tác dụng |
|---|---|
throttle:gp247-api-write |
Giới hạn ghi theo từng tài khoản quản trị, mặc định 300 yêu cầu / phút (biến môi trường GP247_API_WRITE_THROTTLE, dạng số-yêu-cầu,số-phút); vượt → 429 |
api.idempotent |
Bắt buộc header Idempotency-Key (thiếu → 400 idempotency_key_required). Gửi lại cùng khoá, cùng thân → trả lại kết quả lần đầu kèm header Idempotent-Replayed: true, không chạy lại; cùng khoá, thân khác → 409 idempotency_key_reused |
api.idempotent:optional |
Nhận khoá nếu có, không bắt buộc. Dùng api.idempotent cho lệnh mà chạy hai lần sẽ sai (ví dụ cộng/trừ tồn kho) |
Route::group([
'prefix' => GP247_API_V2_CORE_PREFIX, // => api/v2/core
'middleware' => [
'auth:admin-api',
\GP247\Core\Api\Support\ApiAbilities::admin(),
'api.admin:admin_report.sales,write', // a write on the matching admin screen
'throttle:gp247-api-write',
'api.idempotent:optional',
],
], function () {
$ctrl = gp247_namespace(\App\GP247\Plugins\MyApi\Api\Admin\ReportWriteController::class);
Route::post('report/{id}/status', $ctrl.'@status');
});
Trong controller: tìm bản ghi qua ApiStoreScope (ngoài cửa hàng của tài khoản → 404), chỉ nhận các trường trong danh sách
cho phép (từ chối khoá lạ, không lặng lẽ bỏ qua), và đổi trạng thái qua model / method nghiệp vụ — không ghi SQL trần:
use GP247\Core\Api\Support\ApiErrorResponder;
use GP247\Core\Api\Support\ApiStoreScope;
use GP247\Core\Api\V2\ApiQuery;
/**
* `POST report/{id}/status` — change the status of one report.
*/
public function status(Request $request, $id)
{
$report = ApiStoreScope::apply(MyReport::query())->where('id', (string) $id)->first();
if ($report === null) {
return ApiErrorResponder::notFound(); // 404: not in the account's store
}
$rules = ['status' => 'required|integer|in:0,1'];
foreach (array_diff(array_keys($request->all()), array_keys($rules)) as $unknown) {
$rules[$unknown] = 'prohibited'; // unknown keys → 422, never silently dropped
}
$report->changeStatus((int) $request->validate($rules)['status']); // business method, not raw SQL
return ApiQuery::one($report->fresh(), fn ($r) => ['id' => $r->id, 'status' => (int) $r->status]);
}
b) Danh sách sẵn sàng cho đồng bộ. Truyền changedSince: true cho ApiQuery::page() — danh sách nhận thêm
updated_since và phân trang bằng con trỏ page[after] (khi đó sort và page[number] bị từ chối bằng 422):
return ApiQuery::page(ApiStoreScope::apply(MyReport::query()), $request,
fn ($r) => ['id' => $r->id, 'status' => (int) $r->status],
['status' => fn (Builder $q, string $v) => $q->where('status', $v)], // filter[status]
['created_at' => 'created_at'], // sort (normal mode only)
changedSince: true);
- Bảng phải có cột
updated_at(nên có chỉ mục trên cột này). - Mọi lệnh ghi phải làm
updated_atmới lên — lưu qua Eloquent (save()/update()) thì tự có; cập nhật bằng SQL trần hay query builder thì bản ghi không hiện ra trong danh sáchupdated_since.
c) Loại sự kiện của plugin. Khai báo trong config gp247-config.api.events, dạng
'<tiền tố>.<tài nguyên>.<hành động>' => ['fields' => [...], 'permission' => '<tên route màn admin>']. Gộp vào mảng
đang có, không bao giờ ghi đè (core và gói shop cũng đăng ký loại của mình ở đây):
// In the plugin's service provider (boot)
config(['gp247-config.api.events' => array_merge((array) config('gp247-config.api.events', []), [
'myapi.report.status_changed' => [
'fields' => ['from', 'to'], // the only keys ever kept in data
'permission' => 'admin_report.sales', // admins allowed to open this screen may read the event
],
])]);
-
Tên viết thường, các đoạn cách nhau bằng dấu chấm, tối đa 64 ký tự; plugin đặt tiền tố là khoá của plugin để không trùng với loại khác. Tên sai dạng bị bỏ qua.
-
Ghi sự kiện bằng
\GP247\Core\Api\Sync\ApiEvents::record($type, $subjectId, $storeId, $data):\GP247\Core\Api\Sync\ApiEvents::record('myapi.report.status_changed', (string) $report->id, (string) $report->store_id, ['from' => $old, 'to' => (int) $report->status]); -
Chỉ các trường đã khai báo trong
fieldsđược giữ; không bao giờ đưa dữ liệu cá nhân vàodata(ERP cần thêm thì đọc lại bản ghi qua endpoint chi tiết).sourcevàactordo core tự điền. -
Sự kiện được ghi sau khi giao dịch bao quanh commit — giao dịch rollback thì không có sự kiện. Chủ site đang tắt nhật ký sự kiện (hoặc site chưa chạy
php artisan gp247:update) thì không có gì được ghi. Lỗi khi ghi chỉ được báo vào log, không bao giờ ném ra — thay đổi của plugin không bị hỏng vì nhật ký. -
Sự kiện đã ghi cũng được gửi tới các webhook theo dõi loại đó; loại mới tự hiện trong ô chọn loại sự kiện của webhook.
d) Để thay đổi của plugin có sự kiện của cửa hàng (đơn hàng, tồn kho, sản phẩm, khách hàng): lưu qua model của gói shop
(Eloquent save() / update() / delete()) hoặc các method của gói shop. Thay đổi ghi bằng SQL trần hay update() của query
builder không được nhận ra — ERP sẽ không thấy.
5.3. Chọn trường, lấy kèm, lưu tạm và ngừng hỗ trợ
ℹ️ Có từ: gp247/core 3.8.0
Endpoint của bạn hưởng sẵn ETag / 304 cho mọi GET v2 thành công qua GP247_API_MIDDLEWARE — không cần làm gì. Ba việc
còn lại tuỳ endpoint:
fields/include: khai báoApiShape(tên loại, các trường, quan hệ được lấy kèm) rồi truyền lựa chọn vàoApiQuery::page()/ApiQuery::one(). Trường gắn với quan hệ nào thì$shape->with()bỏ nạp quan hệ đó khi trường không được chọn — hàm$toArraykhông được đọc quan hệ của trường đã bị bỏ (dùngApiShapeSelection::build()với closure cho giá trị nặng). Quan hệ lấy kèm phải đi qua cùng luật hiển thị như khi đọc riêng.api.cache.public: chỉ gắn cho route công khai không bao giờ chứa dữ liệu riêng của một người (danh mục, nội dung). Không gắn cho giỏ hàng, tài khoản, đơn hàng hay API quản trị.api.deprecated:<ngày ngừng>,<ngày dừng>,<neo tài liệu>: gắn cho route sắp gỡ; báo trước ít nhất 12 tháng và 2 bản minor.
use GP247\Core\Api\V2\ApiQuery;
use GP247\Core\Api\V2\ApiShape;
// Declare the fields of the resource; a field mapped to relations lets ApiShape skip loading them.
$shape = (new ApiShape('myapi_item', ['name' => ['descriptions'], 'price', 'status'], [
'brand' => [
'with' => ['brand' => fn ($q) => $q->where('status', 1)],
'render' => fn ($item) => $item->brand ? ['id' => $item->brand->id, 'name' => $item->brand->name] : null,
'type' => 'brand',
'fields' => ['id', 'name'],
],
]))->resolve($request); // 422 for an unknown type, field or include
$query = MyItem::query()->with($shape->with(['descriptions']));
return ApiQuery::page($query, $request, fn ($item) => [
'id' => $item->id, 'name' => $item->descriptions->first()?->name, 'price' => (float) $item->price, 'status' => (int) $item->status,
], [], [], null, false, $shape);
// Routes/api.php — a public route without anyone's own data may be cached by browsers / a CDN
Route::get('myapi/items', $controller.'@index')->middleware('api.cache.public');
// A route you plan to remove: Deprecation + Sunset + Link headers on every answer
Route::get('myapi/old-items', $controller.'@old')->middleware('api.deprecated:2026-11-01,2027-12-31,myapi-items');
6. Danh sách kiểm tra trước khi coi là "xong"
- Endpoint mới nằm dưới v2 — dùng hằng số
GP247_API_V2_CORE_PREFIX/GP247_API_V2_FRONT_PREFIX, không hardcode chuỗi, không thêm vào v1. - Controller trỏ qua
gp247_namespace(...)để cho phép override. - Route trong plugin tự bọc
GP247_API_MIDDLEWARE+prefix(mục 4) hoặc đăng ký quaApiRouteRegistrar(mục 4.1). - Có
$request->validate([...])cho mọi input; không ghép chuỗi SQL (dùng Eloquent). - Danh sách dùng
ApiQuery::page()(chỉ khai báo cácfilter/sortcần thiết); một bản ghi dùngApiQuery::one(). - Endpoint riêng tư có
auth:admin-api+ApiAbilities::admin()hoặcauth:customer-api+ApiAbilities::customer()+api.customer.active. - Endpoint quản trị có
api.admin:<tên route admin>và lọc dữ liệu bằngApiStoreScope::apply()(mục 5.1). - Endpoint ghi quản trị có
api.admin:<tên route admin>,write+throttle:gp247-api-write+api.idempotent(hoặcapi.idempotent:optional); controller tìm bản ghi quaApiStoreScope, từ chối trường lạ, đổi trạng thái qua model / method nghiệp vụ (mục 5.2). - Danh sách cần đồng bộ dùng
ApiQuery::page(..., changedSince: true); bảng có cộtupdated_at(có chỉ mục) và mọi lệnh ghi làm mới cột này (mục 5.2). - Loại sự kiện của plugin được gộp vào
gp247-config.api.events(tiền tố là khoá plugin, khai báofieldsvàpermission), ghi bằngApiEvents::record(), không có dữ liệu cá nhân trongdata(mục 5.2). - Thay đổi dữ liệu cửa hàng lưu qua model / method của gói shop (không SQL trần) để ERP nhận được sự kiện (mục 5.2).
- Lỗi trả qua
ApiErrorResponder(mã HTTP đúng nghĩa); hàm$toArraychỉ trả các trường đã khai báo. - Không sửa
vendor/gp247/*(trừ khi bạn là maintainer của gói đó). - Mỗi method có PHPDoc tiếng Anh.
- Đã
php artisan route:clearvà xác nhận route hiện ra trongphp artisan route:list. - Plugin thêm bước xác thực đăng nhập: cài
LoginChallenge, không trả bí mật trongdescribe()(mục 7). - Danh sách/chi tiết khai
ApiShapenếu muốn nhậnfields/include; quan hệ lấy kèm theo đúng luật hiển thị; chỉ route công khai không có dữ liệu riêng mới gắnapi.cache.public; route sắp gỡ gắnapi.deprecated(mục 5.3).
7. Thêm bước xác thực đăng nhập (MFA) từ plugin
ℹ️ Có từ: gp247/core 3.6.0
Core không tự làm OTP; plugin của bạn gắn bước xác thực thứ hai vào đăng nhập API (v2 khách và quản trị) bằng interface
GP247\Core\Api\Contracts\LoginChallenge:
| Method | Khi nào được gọi | Bạn trả về |
|---|---|---|
required($owner, $request) |
Ngay sau khi mật khẩu đúng (cả ở v1) | true nếu tài khoản này phải qua bước xác thực — không gửi mã ở đây |
describe($owner) |
Một lần, khi cấp vé (v2) | Gửi mã SMS/email ở đây nếu cần; trả gợi ý công khai: method, meta — không chứa bí mật |
verify($owner, $request) |
Khi ứng dụng gọi POST login/challenge |
true nếu câu trả lời (trường do bạn đặt, ví dụ code) đúng |
<?php
namespace App\GP247\Plugins\MyOtp;
use GP247\Core\Api\Contracts\LoginChallenge;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Http\Request;
class OtpChallenge implements LoginChallenge
{
public function required(Authenticatable $owner, Request $request): bool
{
return MyOtpSettings::enabledFor($owner); // e.g. the user turned 2FA on
}
public function describe(Authenticatable $owner): array
{
return ['method' => 'totp', 'meta' => ['digits' => 6]]; // public hints only
}
public function verify(Authenticatable $owner, Request $request): bool
{
return MyOtp::check($owner, (string) $request->input('code'));
}
}
// In the plugin's service provider (boot)
config(['gp247-config.api.login_challenge.customer' => array_merge(
config('gp247-config.api.login_challenge.customer', []), [OtpChallenge::class]
)]);
Core lo phần còn lại: vé dùng một lần (5 phút, lưu dạng băm trong cache mặc định — chạy được trên shared host), giới hạn số lần
thử (dùng chung bộ đếm đăng nhập; mỗi vé bị huỷ sau 5 lần sai, từ bất kỳ IP nào), cấp token/phiên khi verify() trả true. Plugin ném lỗi hoặc khai báo sai class → đăng nhập
bị từ chối (không bao giờ bỏ qua bước xác thực). Đăng ký cho quản trị viên: khoá gp247-config.api.login_challenge.admin.
Phía ứng dụng gọi thế nào: Giới thiệu API — mục 9.4.
8. Plugin xử lý đơn hàng: đơn từ ứng dụng đi qua cùng điểm cắm
ℹ️ Có từ: gp247/shop 3.5.0
Đơn đặt qua API (POST api/v2/front/checkout/place) chạy đúng mã checkout của website. Plugin đang xử lý đơn web — tách đơn theo
nhà bán, tính hoa hồng, trừ suất khuyến mãi, gửi thông báo… — nhận đơn từ ứng dụng mà không cần viết thêm gì cho API:
| Điểm cắm | Khi nào chạy | Ghi chú |
|---|---|---|
Sự kiện model ShopOrder::created |
Khi ghi đơn | Dòng sản phẩm được ghi sau, trong cùng giao dịch — đọc chúng trong DB::afterCommit() |
gp247_listen_order_created($order) |
Ngay sau khi tạo đơn | Hàm helper, site tự định nghĩa lại được |
endApp() trong AppConfig của plugin thanh toán / vận chuyển / tổng tiền |
Khi hoàn tất đơn | Mã đơn đọc từ session('orderID') như trên website |
gp247_listen_order_success($order) |
Khi hoàn tất đơn | Đơn qua API: chạy ngay khi đặt, kể cả đơn trả qua cổng (chưa có tiền) |
gp247_order_process_after_success($orderId) |
Sau sự kiện thành công | Gửi email xác nhận đơn |
Muốn thay một hàm helper ở bảng trên, thêm tên hàm vào config/gp247_functions_except.php rồi tự định nghĩa hàm đó (cùng chữ ký).
// In the plugin's service provider (boot)
\GP247\Shop\Models\ShopOrder::created(function ($order) {
\Illuminate\Support\Facades\DB::afterCommit(function () use ($order) {
// The order lines exist here, whether the order came from the website or the app.
MyCommission::book($order->fresh('details'));
});
});
Ba điều cần nhớ khi plugin chạy trong request API:
- Nguồn đơn: cột
channelcủa đơn làapicho đơn từ ứng dụng,webcho đơn từ website (đơn tạo trong trang quản trị và đơn cũ để trống). Chỉ dùng khi thật sự cần phân biệt — đa số plugin không cần. - Không có phiên trình duyệt: trong request API,
session()là phiên riêng của request đó (không cookie). Đọc / ghi khoá checkout như trên website vẫn đúng, nhưng đừng redirect, flash message hay đọc cookie — ứng dụng không thấy chúng. - "Đơn thành công" không có nghĩa "đã trả tiền": với đơn qua API, sự kiện chạy ngay khi đặt. Việc chỉ làm sau khi đã trả tiền (giao hàng số, mở khoá dịch vụ…) hãy dựa vào trạng thái thanh toán của đơn.
Phía ứng dụng gọi thế nào: Giới thiệu API — mục 10.
Hỏi & Đáp (Q&A)
Câu 1: Tôi nên thêm API vào đâu — vendor hay plugin?
→ Gần như luôn là plugin (app/GP247/Plugins/) hoặc override qua app/GP247/. Chỉ sửa trực tiếp
vendor/gp247/* khi bạn là người bảo trì chính của gói đó, vì mọi thay đổi trong vendor sẽ mất khi
composer update.
Câu 2: Tại sao route mới của tôi không xuất hiện?
→ Ba nguyên nhân phổ biến: (1) chưa bật GP247_API_MODE=true trong .env; (2) chưa xóa cache —
chạy php artisan route:clear; (3) với plugin, file phải tên đúng Route.php và plugin đang được
kích hoạt.
Câu 3: API từ plugin của tôi không trả JSON / không bị giới hạn tần suất?
→ Vì route plugin nạp ngoài nhóm middleware API. Bạn phải tự bọc 'middleware' => GP247_API_MIDDLEWARE
như ở mục 4. Đây là lỗi hay gặp nhất khi thêm API từ plugin.
Câu 4: ApiQuery::page() khác gì paginate() của Laravel?
→ ApiQuery::page() dùng paginate() bên dưới nhưng thêm phần chuẩn của API v2: chỉ nhận các filter[...] / sort
bạn khai báo (tên lạ trả 422), giới hạn page[size] tối đa 100, luôn sắp thêm theo khoá chính để các trang không
trùng/sót bản ghi, và trả đúng dạng {data, meta, links}. Hãy dùng nó cho các endpoint danh sách để đồng bộ với các
API v2 sẵn có của GP247.
Câu 5: Làm sao trả về lỗi cho đúng chuẩn?
→ Dùng ApiErrorResponder (notFound(), forbidden(), respond($status, $code, $msg)) — thân lỗi
{error, msg, code} với mã HTTP đúng nghĩa. Lỗi xác thực, validate và giới hạn tần suất GP247 tự đổi
sang dạng này. Không trả HTML hay chuỗi trần.
Câu 6: Tôi cần scope/quyền riêng cho API của mình?
→ Scope chỉ phân biệt loại tài khoản (admin, admin-supper, user). Quyền xem dữ liệu của endpoint
quản trị lấy từ phân quyền màn admin qua api.admin:<tên route admin> (mục 5.1) — người quản trị cấp
quyền một lần cho cả trang quản trị lẫn API. Tránh tự phát minh cơ chế quyền song song.
Câu 7: Lớp apiconnection/apikey có bắt buộc cho API mới của tôi không?
→ Không do bạn quyết định trong code — nó bật/tắt toàn cục qua cấu hình api_connection_required trong
quản trị. Miễn là route của bạn nằm trong nhóm GP247_API_MIDDLEWARE, lớp này áp dụng tự động khi
admin bật.
Câu 8: Tôi có phải tự viết endpoint đăng nhập/cấp token không?
→ Không. Hãy tái dùng api/v2/core/login (admin) hoặc api/v2/front/login (khách) để lấy token, rồi bảo vệ
endpoint mới bằng auth:admin-api/auth:customer-api. Chỉ tự viết login khi có luồng xác thực đặc thù.
Câu 9: Controller API nên kế thừa class nào?
→ Dữ liệu admin → kế thừa GP247\Core\Controllers\RootAdminController; dữ liệu front/cửa hàng →
kế thừa GP247\Front\Controllers\RootFrontController. Chúng đã thiết lập sẵn ngữ cảnh phù hợp cho
từng phía.
Câu 10: Có cần viết test cho API mới không?
→ Nên có — viết feature test (PHPUnit) cho endpoint, đặt trong thư mục test của dự án hoặc plugin của bạn. Ít nhất kiểm tra: mã trạng thái, cấu trúc JSON, và chặn truy cập khi thiếu token với endpoint riêng tư.
Lịch sử thay đổi
| Ngày | Phiên bản GP247 | Thay đổi |
|---|---|---|
| 2026-11-02 | gp247/core 3.8.0 | Endpoint v2 có sẵn ETag / 304; plugin khai ApiShape để nhận fields / include, gắn api.cache.public cho route công khai và api.deprecated cho route sắp gỡ (mục 5.3, 6) |
| 2026-11-01 | gp247/core 3.7.0, gp247/shop 3.6.0 | Endpoint ghi quản trị: api.admin:<route>,write (quyền như thao tác ghi trên màn admin), throttle:gp247-api-write, api.idempotent / api.idempotent:optional; danh sách đồng bộ bằng ApiQuery::page(..., changedSince: true); plugin đăng ký loại sự kiện qua gp247-config.api.events và ghi bằng ApiEvents::record(); lưu qua model của gói shop để có sự kiện của cửa hàng (mục 5.2, 6) |
| 2026-10-31 | gp247/core 3.6.0, gp247/shop 3.5.0; API v2: core/front/shop 3.3.0 | Endpoint mới đặt dưới tiền tố v2 (GP247_API_V2_CORE_PREFIX, GP247_API_V2_FRONT_PREFIX), dùng ApiQuery cho phản hồi {data, meta, links}; v1 đóng băng (mục 1–6); json.response thuộc GP247_API_MIDDLEWARE và chạy trước auth:* — thiếu Accept không còn ra 500 (mục 1); đơn đặt qua API v2 đi qua cùng điểm cắm với đơn web (ShopOrder::created, endApp(), gp247_listen_order_success, email xác nhận); cột channel của đơn (api / web) (mục 8) |
| 2026-10-28 | gp247/core 3.6.0 | Plugin thêm bước xác thực đăng nhập API (MFA) qua LoginChallenge (mục 7) |
| 2026-10-26 | gp247/core 3.2.3; seam đăng ký route: 3.3.0 | Mỗi gói/plugin đăng ký route API qua ApiRouteRegistrar, thêm thư mục Routes/Api/v2/, ApiAbilities::admin() (3.3.0); endpoint quản trị phải khai báo màn admin (api.admin) và lọc theo cửa hàng (ApiStoreScope); lỗi trả qua ApiErrorResponder với mã HTTP đúng nghĩa; lọc trường dữ liệu công khai bằng ApiFieldAllowlist |
📅 Cập nhật lần cuối: 2026-11-02 · ✍️ Tác giả (Author): GP247