seba1rx/tabmanager
最新稳定版本:v1.2.0
Composer 安装命令:
composer require seba1rx/tabmanager
包简介
Per-tab PHP session isolation — each browser tab gets its own namespaced $_SESSION slot via a UUIDv4 header
README 文档
README
Package name: seba1rx/tabmanager
TabManager provides per-tab session isolation for PHP applications. It ensures each browser tab maintains its own independent session state, preventing shared session data between tabs — useful for admin dashboards, multi-step forms, or any multi-tab workflow.
How It Works
Each browser tab receives a unique UUIDv4 generated by the JS client and stored in sessionStorage. On every AJAX request the UUID is sent as the X-TabManager-TabId header. The PHP backend reads this header to isolate $_SESSION data under that UUID.
Why a header instead of a cookie? Cookies are shared across all tabs for the same domain — if Tab B sets a new tab-ID cookie, Tab A's cookie is overwritten immediately. The header is sent per-request with the correct UUID from sessionStorage, which is strictly per-tab.
Duplicate-tab detection: when a browser duplicates a tab it copies sessionStorage, so the new tab would inherit the original's UUID. On init the JS client uses the BroadcastChannel API to ask whether any other running tab already claims that UUID. If yes, a fresh UUID is generated before the tab registers with the server.
Heartbeat: while a tab is visible the JS client POSTs to the heartbeat endpoint every 30 s, keeping last_active accurate on the server. The heartbeat pauses when the browser hides or freezes the tab (Page Visibility API) and resumes — with an immediate beat — when the tab becomes visible again.
AI-assisted implementation
The package ships with llms.txt — a structured reference document written for AI coding assistants. It covers the full integration flow, every method and configuration option, security constraints, common mistakes, and a minimal working example.
If you are implementing this package with the help of an AI assistant (Claude, Copilot, Cursor, etc.), point it at that file before asking it to write integration code:
Read llms.txt before implementing seba1rx/tabmanager.
Implementation Guide
Step 1 — Install
composer require seba1rx/tabmanager
The Composer post-install script copies seba1rx_tabmanagerclient.js to your project root automatically.
Step 2 — Include the JS client in your HTML
Add the script tag before your own application scripts. Optionally set configuration globals in an inline script before it.
<head> <!-- Optional: override defaults before the client loads --> <script> // Only needed if /tabmanager/* routes are not available in your setup. // For example, when using php -S without a router. // window.TABMANAGER_HEARTBEAT_URL = '/your/custom/heartbeat'; // Uncomment to enable verbose JS logging during development: // window.TABMANAGER_DEBUG = true; </script> <script src="/seba1rx_tabmanagerclient.js"></script> </head>
The client runs automatically on DOMContentLoaded. It assigns a UUID to the tab, detects duplicates, sets a fallback cookie, notifies the backend, and starts the heartbeat.
Step 3 — Await TabManagerClient.ready before using the tab ID
init() is async (it waits ~80 ms for the BroadcastChannel duplicate check). If your scripts read TabManagerClient.tab.id or call getHeaders() before init completes they will get null.
document.addEventListener('DOMContentLoaded', async () => { await TabManagerClient.ready; // wait for UUID to be confirmed console.log('Tab ID:', TabManagerClient.tab.id); // safe to use getHeaders() from here });
Step 4 — Add the tab header to every AJAX call
Spread TabManagerClient.getHeaders() into the headers object of every fetch or XMLHttpRequest that talks to your PHP backend. This sends X-TabManager-TabId: <uuid> so the server knows which tab is making the request.
const response = await fetch('/your-endpoint', { method: 'POST', headers: { 'Content-Type': 'application/json', ...TabManagerClient.getHeaders(), // <-- adds X-TabManager-TabId }, body: JSON.stringify(payload), });
Calls that omit
getHeaders()will fall back to theTABMANAGER_TABIDcookie, which is shared across tabs and therefore unreliable for tab isolation.
Step 5 — Use TabManager in your PHP endpoints
The bootstrap file runs automatically on every require 'vendor/autoload.php', registering the internal endpoints. You do not need to include or configure anything extra.
In any PHP file that handles a request from the JS client:
<?php require_once __DIR__ . '/vendor/autoload.php'; // bootstrap runs here use Seba1rx\TabManager\TabManager; $tabManager = new TabManager(); // Write tab-specific data $tabManager->set('user_step', 3); $tabManager->set('draft_id', 42); // Read it back (from the same tab only) $step = $tabManager->get('user_step'); // 3
TabManager::set() and get() resolve the tab UUID from the X-TabManager-TabId header automatically — you never handle the UUID manually.
Step 6 — (Optional) Expose a heartbeat endpoint
The JS client POSTs to /tabmanager/heartbeat by default. This endpoint is registered automatically by the bootstrap in any setup where all requests are routed through a PHP front controller (the standard setup for frameworks and SPAs with a catch-all route).
If your setup does not route /tabmanager/* to PHP (e.g. php -S without a router, or a framework that only routes registered paths), create a dedicated PHP file and point the client at it:
<script> window.TABMANAGER_HEARTBEAT_URL = '/heartbeat-tab.php'; </script>
<?php // heartbeat-tab.php require_once __DIR__ . '/vendor/autoload.php'; $tabId = $_SERVER['HTTP_X_TABMANAGER_TABID'] ?? null; if ($tabId) { $tm = new Seba1rx\TabManager\TabManager(); $tm->touchTab($tabId); } http_response_code(204);
HTTP Endpoints
Registered automatically on every require 'vendor/autoload.php':
| Method | Endpoint | Description |
|---|---|---|
POST |
/tabmanager/new-tab |
Register a new tab in session |
POST |
/tabmanager/heartbeat |
Update last_active for a tab |
POST |
/tabmanager/tab-close |
Mark a tab as inactive |
GET |
/tabmanager/tab-status |
Returns { "indexed": bool } — whether the tab still exists in session |
GET |
/tabmanager/debug_js |
Session dump as JSON (debug only) |
GET |
/tabmanager/debug_html |
Session dump as HTML table (debug only) |
POST |
/tabmanager/debug/delete-tab |
Delete a tab session (debug only) |
These endpoints intercept the request before your application router runs and call
exit. They are invisible to your framework.
Debug endpoints are only accessible when define('TABMANAGER_DEBUG', true) is set. There is no IP-based fallback — REMOTE_ADDR checks were removed because reverse proxies always report 127.0.0.1 as the client IP.
PHP Class Reference
| Method | Description |
|---|---|
indexNewTab(string $tabId) |
Registers a new tab in session (idempotent) |
touchTab(string $tabId) |
Updates last_active and is_active; creates the tab if missing |
set(string $key, mixed $value) |
Stores a value in the current tab's session slot |
get(string $key, mixed $default = null) |
Retrieves a value from the current tab's session slot |
destroyTabSession(string $tabId) |
Deletes all session data for a tab |
markInactiveTab(string $tabId) |
Sets is_active = false (called on beforeunload) |
isTabIndexed(?string $tabId = null): bool |
Returns whether a tab is registered; resolves current tab from header/cookie when no argument given |
cleanupInactiveTabs(int $olderThanSeconds): int |
Removes inactive tabs older than the given threshold; returns count deleted |
getTabIdStrict(): ?string |
Reads X-TabManager-TabId header only — no cookie fallback. Use for sensitive endpoints. |
debug(): array |
Returns a summary of all tab sessions |
uuid_v4(): string |
Generates a UUIDv4 (utility for consumers) |
is_valid_uuid(string $uuid, bool $onlyV4) |
Validates UUID format (utility for consumers) |
Custom session store
By default TabManager manages the session itself using native PHP functions (session_start(), $_SESSION). If your application already handles the session — for example, through a session-management package — you can delegate session responsibility to a custom class and pass it to the constructor.
The contract
Seba1rx\TabManager\Contracts\SessionStoreInterface defines six methods:
interface SessionStoreInterface { public function start(): void; // start if not already active (idempotent) public function isActive(): bool; // whether the session is currently active public function get(string $key, mixed $default = null): mixed; public function set(string $key, mixed $value): void; public function has(string $key): bool; public function remove(string $key): void; }
Using the default store (standard setup)
When no store is provided, TabManager instantiates PhpSessionStore internally:
$tm = new TabManager(); // uses PhpSessionStore → calls session_start() if needed
Injecting a custom store
Implement the interface and pass your store to the constructor:
use Seba1rx\TabManager\Contracts\SessionStoreInterface; use Seba1rx\TabManager\TabManager; class MySessionStore implements SessionStoreInterface { public function start(): void { /* session already started by your framework */ } public function isActive(): bool { return /* check your session driver */; } public function get(string $key, mixed $default = null): mixed { /* ... */ } public function set(string $key, mixed $value): void { /* ... */ } public function has(string $key): bool { /* ... */ } public function remove(string $key): void { /* ... */ } } $tm = new TabManager(new MySessionStore());
TabManager calls $store->start() in the constructor. If the session is already active your start() implementation can be a no-op. All set(), get(), has(), and remove() calls use the store root — TabManager manages its own 'tabmanager' key internally.
JS Client Reference
| Member | Description |
|---|---|
TabManagerClient.ready |
Promise that resolves when init() completes. Always await before using tab.id. |
TabManagerClient.tab.id |
The confirmed UUID for this tab. null until ready resolves. |
TabManagerClient.getHeaders() |
Returns { 'X-TabManager-TabId': <uuid> }. Spread into every AJAX call. |
TabManagerClient.heartbeat.send() |
Sends one heartbeat manually. |
TabManagerClient.heartbeat.start() |
Starts the heartbeat interval. |
TabManagerClient.heartbeat.stop() |
Stops the heartbeat interval. |
JS Configuration
Set these globals before loading the script:
| Variable | Default | Description |
|---|---|---|
window.TABMANAGER_DEBUG |
false |
Enables verbose console logging |
window.TABMANAGER_HEARTBEAT_URL |
/tabmanager/heartbeat |
Heartbeat endpoint URL |
window.TABMANAGER_HEARTBEAT_INTERVAL |
30000 |
Heartbeat interval in milliseconds |
window.TABMANAGER_NEW_TAB_URL |
/tabmanager/new-tab |
Override new-tab registration endpoint |
window.TABMANAGER_TAB_CLOSE_URL |
/tabmanager/tab-close |
Override tab-close notification endpoint |
window.TABMANAGER_TAB_STATUS_URL |
/tabmanager/tab-status |
Override tab-status check endpoint |
Detecting session loss after tab suspension
When a browser suspends a tab (e.g. Chrome Memory Saver) the JS heartbeat stops. If your app calls cleanupInactiveTabs() in the meantime, the tab's session slot may be deleted. When the user returns to the tab, TabManager detects this automatically and dispatches a tabmanager:session-lost event on document.
Listen for it to show a warning — the UI is entirely up to your app:
document.addEventListener('tabmanager:session-lost', (e) => { // e.detail.tabId — the UUID of the lost tab alert('Your session data was removed due to inactivity. Please reload.'); });
The check runs silently on every visibilitychange to visible. If the endpoint is unreachable (network error, 404) the event is not fired — the client fails open to avoid false positives.
For php -S setups without a router, point the client at a physical file:
<script>window.TABMANAGER_TAB_STATUS_URL = '/tab-status.php';</script>
<?php // tab-status.php require_once __DIR__ . '/vendor/autoload.php'; $tabId = $_SERVER['HTTP_X_TABMANAGER_TABID'] ?? null; $indexed = $tabId && Seba1rx\TabManager\TabManager::isValidTabId($tabId) ? (new Seba1rx\TabManager\TabManager())->isTabIndexed($tabId) : false; header('Content-Type: application/json'); echo json_encode(['indexed' => $indexed, 'tab_id' => $tabId]);
Session Data Structure
$_SESSION['tabmanager']['tabs'] = [ '<uuid>' => [ 'data' => [], // key-value store for this tab 'is_active' => true, 'last_active' => 1234567890, // unix timestamp, updated by heartbeat ], ];
Debug Interface
Enable debug mode in PHP:
define('TABMANAGER_DEBUG', true); require_once __DIR__ . '/vendor/autoload.php';
Then visit /tabmanager/debug_html for a table showing all tabs, their status, last active time, stored keys, and a delete button per tab.
Enable JS logging:
<script>window.TABMANAGER_DEBUG = true;</script>
Security notes
TABMANAGER_DEBUGmust never be set in production. The debug endpoints expose internal session structure. There is no IP-based fallback — the constant is the only access gate.- Session ID regeneration is the application's responsibility. This package does not call
session_regenerate_id(). The consuming application must call it on login and privilege-change events to prevent session fixation attacks. - Sensitive endpoints should use header-only tab resolution.
TabManager::getTabIdStrict()returns only theX-TabManager-TabIdheader value, ignoring the shared cookie fallback. Use it in endpoints where tab impersonation via cookie injection would be harmful.
Unit tests
The package includes a PHPUnit test suite covering all public methods of TabManager.
composer test
What is tested:
| Class / method | Scenarios covered |
|---|---|
isValidTabId() |
Valid UUID v4 (lower/upper/mixed case), wrong version digit, wrong variant nibble, empty string, arbitrary string, too short, too long, invalid hex chars, missing hyphens |
__construct() |
Session structure initialization, existing data not overwritten, other session keys untouched, default store (PhpSessionStore), custom store injection |
indexNewTab() |
data initialized as empty array, is_active defaults to true, last_active set to current time, idempotency |
touchTab() |
last_active updated, inactive tab reactivated, data not modified, creates tab if not registered |
set() |
Value stored correctly, last_active updated, tab reactivated, no-op when tab not registered (SEC-04), no-op when no tab ID present, key overwrite, cookie fallback, various value types |
get() |
Returns stored value, returns null for absent key, returns custom default, returns default when tab not registered, returns default when no tab ID |
markInactiveTab() |
Sets is_active = false, data and last_active preserved, no-op for unregistered tab |
destroyTabSession() |
Removes tab entry, sibling tabs unaffected, no-op for unregistered tab |
isTabIndexed() |
Returns true for registered tab, false for unregistered, explicit UUID argument, resolves current tab from header, returns false when no ID present |
cleanupInactiveTabs() |
Removes stale inactive tab, preserves active tab, preserves recent inactive tab, returns 0 when nothing to clean, no-op for threshold ≤ 0 |
debug() |
Empty array when no tabs, correct structure (is_active, last_active, keys, size), last_active formatted as Y-m-d H:i:s, keys contains data key names, size matches json_encode byte length |
getTabIdStrict() |
Returns header value, returns null when only cookie present, returns null when nothing present |
Tests use an in-memory ArraySessionStore (implements SessionStoreInterface) — no PHP session functions, no files written, fully isolated.
Integration with seba1rx/sessionadmin
If your application uses seba1rx/sessionadmin for session management, use SessionAdminBridge instead of TabManager directly. The bridge implements TabHandlerInterface (defined in sessionadmin) and lets SessionAdmin own the session lifecycle, so both packages share the same PHP session without conflict.
composer require seba1rx/sessionadmin
use Seba1rx\TabManager\Bridge\SessionAdminBridge; $session = new App\MySession(); $session->setTabHandler(new SessionAdminBridge()); $session->autoCleanupTabs = 30; // optional: remove tabs inactive for > 30 s $session->activateSession(); // configures session name/lifetime, then starts session // After the JS client registers the tab: $session->tabHandler->set('cart', ['apple' => 3]); $cart = $session->tabHandler->get('cart'); $ready = $session->tabHandler->isTabIndexed(); // false until JS fires
SessionAdminBridge does not call session_start() in its constructor — SessionAdmin starts the session with the correct name and cookie parameters. Once the session is active, all tab methods work normally.
Bootstrap endpoints and custom session names: When SessionAdmin configures a custom session name (e.g. $this->sessionName = 'my_app'), the tabmanager bootstrap endpoints (/tabmanager/new-tab, /tabmanager/heartbeat, etc.) must use the same session name. Set session_name() before require 'vendor/autoload.php' in the entry point that handles those requests, or route them through a script that calls $session->activateSession() first.
Requirements
- PHP >= 8.1
License
MIT License © 2025 Seba1rx
统计信息
- 总下载量: 0
- 月度下载量: 0
- 日度下载量: 0
- 收藏数: 0
- 点击次数: 1
- 依赖项目数: 0
- 推荐数: 0
其他信息
- 授权协议: MIT
- 更新时间: 2026-06-08