Bốn state nhìn lướt qua
Mỗi row plugin có một column status với một trong hai giá trị — active hoặc inactive. Hai state nữa thì ngầm: chưa register (chưa có DB row, chưa có entry master file) và đã deleted (folder file biến mất, row biến mất, entry master file biến mất). Bốn transition di chuyển plugin giữa chúng:
| Transition | Method | Status trước | Status sau | Thay đổi gì trên disk / DB |
| Register | Plugin::register($name) | (không có row) | inactive | Insert DB row; ghi entry master file; service provider boot trong request hiện tại |
| Activate | $plugin->activate() | inactive | active | Migration chạy qua activate hook; flip DB status; clear field error trong master file |
| Disable | $plugin->disable() | active | inactive | Flip DB status; clear field error trong master file. Không gì khác. |
| Delete | $plugin->deleteAndCleanup($keepData = false) | bất kỳ | (không có row) | Delete hook fire (thường migrate:rollback); xoá folder plugin; xoá DB row; xoá entry master file |
Mental model đáng giữ: register và delete thay đổi thế giới (file trên đĩa, DB schema). Activate và disable chỉ flip một flag — route, view, hook, và state service provider từ register vẫn ở yên. Các section tiếp theo đi qua từng transition theo thứ tự.
State 1 — Register / install
Plugin::register($name) tại app/Model/Plugin.php:559 là entry point. Nó được gọi tự động ở cuối php artisan plugin:init và sau mỗi lần upload thành công qua admin Plugins page. Method làm năm việc riêng biệt theo thứ tự:
- Đọc
composer.json từ storage/app/plugins/{vendor}/{name}/ và copy title, description, version vào model. Throw nếu field name trong composer không khớp chính xác với directory.
- Insert (hoặc update) row vào table DB
plugins với status = inactive. firstOrNew(['name' => $name]) là lookup, nên re-register một plugin đã tồn tại sẽ update chứ không duplicate.
- Ghi master file:
storage/app/plugins/index.json được thêm entry { "name": { "status": "inactive" } }. Đây là registry boot-time mà host đọc mỗi request mà không cần đi qua DB.
- Load service provider ngay:
$plugin->load($withServiceProvider = true) đăng ký PSR-4 prefix với một Composer\Autoload\ClassLoader mới và gọi App::register() trên class service provider của plugin. Đến khi method return, route, view, và hook của plugin đã wire vào running process.
- Materialise translation và publish asset:
Language::dump() tạo các runtime file theo locale dưới storage/app/data/plugins/{vendor}/{name}/lang/, rồi artisan vendor:publish --force --tag=plugin copy mọi asset bundled vào public/plugins/{vendor}/{name}/.
Sau register, plugin đã installed và loaded. Nó chưa active — nghĩa là bất cứ thứ gì plugin chọn wire vào event activate của nó thì chưa chạy. Route, view, và hook listener của plugin đã live rồi.
State 2 — Activate
$plugin->activate() tại Plugin.php:484 là cái mà nút admin "Activate" gọi. Bốn bước theo thứ tự:
- Fire activate hook:
Hook::fire('activate_plugin_'.$this->name). Mọi listener đăng ký với name này đều chạy — thường là listener Hook::on('activate_plugin_*', ...) của chính plugin gọi artisan migrate trên folder migration của plugin. Plugin khác có thể đăng ký thêm listener trên cùng event.
- Re-validate
composer.json: self::validateMetaData($config) verify các key bắt buộc của plugin (name, version, app_version) có mặt và đúng form. Key thiếu sẽ throw trước khi flip status.
- Set DB status thành
active và save row.
- Update master file:
{ "status": "active", "error": null } — việc reset error clear mọi boot failure trước đó nên các autoload sweep tiếp theo xem plugin là healthy.
Activation idempotent trên thực tế. Re-run activate() trên một plugin đã active sẽ fire lại hook (nên các listener đã chạy migrate sẽ chạy lại — table migration của Laravel de-dupe các file đã chạy nên lần invocation thứ hai là no-op), re-validate, và ghi cùng status. Không có branch "đã active" đặc biệt.
State 3 — Disable
$plugin->disable() tại Plugin.php:136 là method đơn giản nhất trong bốn method. Nó chỉ làm đúng những việc sau:
- Set DB status thành
inactive.
- Update master file với status mới và clear mọi field
error.
Đó là toàn bộ method. Nó không unload bất cứ gì.
Route đã đăng ký lúc boot() của plugin vẫn đăng ký. View vẫn mount được. Hook listener vẫn fire khi host fire hook của họ. Service provider của plugin vẫn loaded vào container của application và sẽ được load lại ở request tiếp theo vì autoloadWithoutDbQuery() đọc mọi entry từ master file bất kể status. Disable là một flip status, không phải unload — bản thân Laravel không hỗ trợ unregister service provider sau khi nó boot.
Đây là lý do plugin acelle/console là pattern chính tắc "feature plugin nên biến mất khi disable": route luôn load, nhưng có một route middleware tên console.active abort 404 khi Plugin::getByName('acelle/console')->isActive() trả về false. Check chạy mỗi request, dựa vào status DB hiện tại, nên disable plugin làm route của nó trả 404 từ request tiếp theo.
Visible-disable pattern trong ba bước. (1) Define một route middleware check Plugin::enabled('myvendor/myplugin') và abort 404 khi false. (2) Register nó như middleware alias trong boot() của service provider. (3) Apply nó lên route group trong routes.php. Mọi plugin ship feature user-visible nên follow pattern này — không có nó, "deactivated" nhìn giống y "activated" từ góc nhìn user.
State 4 — Delete
$plugin->deleteAndCleanup($keepData = false) tại Plugin.php:670 là teardown đầy đủ. Bốn bước theo thứ tự:
- Fire delete hook:
Hook::fire('delete_plugin_'.$name, [$keepData]). Listener của skeleton gọi artisan migrate:rollback trên folder migration của plugin. Flag $keepData được forward để listener có thể opt out khỏi việc rollback các table giữ data customer — xem trang database-models cho pattern đã worked.
- Xoá folder plugin:
$this->deletePluginDirectory() recursively remove storage/app/plugins/{vendor}/{name}/. Sau bước này, source PHP của plugin biến mất khỏi disk.
- Xoá DB row. Table
plugins không còn reference đến plugin này.
- Xoá entry master file:
updatePluginMasterFile($name, null) — null là signal convention để drop entry chứ không merge field mới.
Cho đến khi request tiếp theo boot một process mới, route, view, và hook của plugin vẫn loaded trong memory — Laravel container in-process không có concept "unregister service provider của plugin này". Request tiếp theo đọc master file (giờ đã thu nhỏ), không load plugin, và state in-memory bị discard cùng lifecycle của request trước.
Master file ở mỗi transition
storage/app/plugins/index.json là source of truth duy nhất lúc boot time. Mọi transition ở trên đều ghi vào nó. Một cách hữu ích để thấy lifecycle là theo dõi entry của một plugin trông thế nào ở mỗi bước:
// Before register: no entry.
{}
// After register:
{
"acmecorp/loyalty": { "status": "inactive" }
}
// After activate:
{
"acmecorp/loyalty": { "status": "active" }
}
// After a boot failure (sticky until cleared by activate):
{
"acmecorp/loyalty": { "status": "active", "error": "Class \"…\" not found" }
}
// After disable (error cleared, status flipped):
{
"acmecorp/loyalty": { "status": "inactive" }
}
// After delete: no entry.
{}
Ba host-side method own file đó: updatePluginMasterFile($name, $params) cho merge-write (pass null là arg thứ hai để remove entry), resetPluginMasterFile() để rebuild file từ Plugin::all() khi nó lệch khỏi DB, và getErroredPluginNames() để đọc mọi entry và trả về các name có error không rỗng.
Recovery từ broken state
Ba failure mode xuất hiện trên production:
1. Row plugin trong master file bị stale hoặc sai
Hay gặp sau khi edit thủ công, deploy partial, hoặc restore database snapshot. Fix: chạy php artisan tinker và gọi Plugin::resetPluginMasterFile(). Method iterate Plugin::all() từ DB và rewrite file JSON từ đầu, preserve status và clear mọi field error.
2. Field error của plugin bị set và admin Plugins page show pill đỏ
Error là sticky — set khi autoloadWithoutDbQuery() wrap call loadPluginByName() trong try/catch và call throw. Error remain cho đến khi hoặc activate() thành công (set error => null) hoặc disable() (cùng vậy). Fix: resolve underlying problem (thiếu autoload.psr-4, namespace mismatch, thiếu class service provider), rồi click Activate; boot tiếp theo sẽ succeed và error sẽ clear.
3. Folder plugin biến mất nhưng entry master file vẫn còn
Xảy ra sau khi rm -rf thủ công. Boot vẫn cố load plugin qua entry master file, throw, và record error. Fix: remove entry master file trực tiếp bằng Plugin::updatePluginMasterFile($name, null), hoặc — nếu plugin vẫn nên tồn tại — re-upload source archive và chạy lại Plugin::register($name) để repopulate mọi thứ.
Các console command plugin:*
Một artisan command ship trong host: plugin:init. Không có command plugin:activate, plugin:disable, hay plugin:delete — đó là các action admin-UI. Truy cập programmatic đi thẳng qua model method:
php artisan tinker
>>> $p = \App\Model\Plugin::getByName('acmecorp/loyalty');
>>> $p->activate(); // → active, runs migration via activate hook
>>> $p->disable(); // → inactive, status flip only
>>> $p->deleteAndCleanup($keepData = true); // → preserve customer-facing tables
Đây là cùng surface mà admin Plugins page dùng nội bộ. CI script, seeder, và integration test đều reach thẳng tới các method này. Deep-dive Testing nói về pattern ở mức test suite.
State transition trong một sơ đồ
┌─────────────────────┐
│ not registered │ (no row, no master-file entry)
└──────────┬──────────┘
│ Plugin::register($name)
│ ├─ writes DB row (status=inactive)
│ ├─ writes master file
│ ├─ loads service provider in-process
│ └─ Language::dump() + vendor:publish
▼
┌─────────────────────┐
┌────▶ │ inactive │ ◀───┐
│ └──────────┬──────────┘ │
│ │ │
│ activate()│ │ disable()
│ │ │ ├─ status=inactive
│ │ │ └─ master file updated
│ ▼ │
│ ┌─────────────────────┐ │
│ │ active │ ────┘
│ └──────────┬──────────┘
│ │
│ deleteAndCleanup($keepData)
│ │ ├─ fires delete hook (rollback unless $keepData)
│ │ ├─ removes plugin folder
│ │ ├─ deletes DB row
│ │ └─ removes master-file entry
│ ▼
│ ┌─────────────────────┐
└──────│ not registered │
└─────────────────────┘
(cycle: register again to re-install)
Năm anti-pattern
1. Coi disable như là unload plugin
Route vẫn register, hook vẫn fire, view vẫn mount. Fix: guard feature user-visible bằng middleware Plugin::enabled(...) hoặc check inline, đúng như acelle/console.
2. Edit thủ công master file trên production
Dễ corrupt JSON. Fix: gọi Plugin::updatePluginMasterFile() hoặc Plugin::resetPluginMasterFile() qua tinker — cả hai đều validate.
3. rm -rf storage/app/plugins/{vendor}/{name} mà không remove entry master
Boot cứ cố load plugin biến mất và record error. Fix: luôn pair việc remove folder với Plugin::updatePluginMasterFile($name, null), hoặc dùng deleteAndCleanup() làm cả hai.
4. Gọi activate() từ bên trong boot() của service provider
Boot phase chạy một lần per process; gọi activate() ở đó fire activate hook mỗi request. Migration chạy mỗi lần (idempotent — nhưng tốn kém), và listener side-effect cũng fire. Fix: activation là action admin-UI, không bao giờ là side effect boot-time.
5. Quên rằng register xảy ra trước activate
Một số plugin cố seed data default qua listener activate hook và reference Eloquent model phụ thuộc vào migration riêng của plugin — nhưng migration chưa chạy lần activate đầu tiên. Fix: listener migration chạy trong activate, trước bất kỳ listener Hook::on('activate_plugin_*') nào có thể reference table mới. Order các đăng ký để migration đi trước (skeleton làm vậy — giữ nguyên).
Đi tiếp đến đâu
Lifecycle phụ trách khi nào; Testing phụ trách verify. Trang tiếp theo đi qua đăng ký testsuite trong phpunit.xml, pattern base-class PluginTestCase, assertion cho hook-under-test, và chu trình CI activate-test-delete.