承接 seba1rx/tabmanager 相关项目开发

从需求分析到上线部署,全程专人跟进,保证项目质量与交付效率

邮箱:yvsm@zunyunkeji.com | QQ:316430983 | 微信:yvsm316

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 the TABMANAGER_TABID cookie, 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_DEBUG must 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 the X-TabManager-TabId header 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

GitHub 信息

  • Stars: 0
  • Watchers: 0
  • Forks: 0
  • 开发语言: PHP

其他信息

  • 授权协议: MIT
  • 更新时间: 2026-06-08

承接程序开发

PHP开发

VUE

Vue开发

前端开发

小程序开发

公众号开发

系统定制

数据库设计

云部署

网站建设

安全加固