F IdentityFormatter contract — apps override how the User column renders
websockets:watch -v now delegates User-column rendering to a bound IdentityFormatter. The package ships DefaultIdentityFormatter which produces #<id> - <name> | <username> - <email> for typical Eloquent users (any field absent = that segment dropped). Apps with non-User auth subjects (Company, ApiClient, multi-tenant blobs) can implement the contract and either bind it in their service provider or name it in config/websockets.php as 'identity_formatter'. Resolution order is: explicit container binding > config class > package default. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
e45d8dff20
commit
a46243a706
|
|
@ -55,6 +55,29 @@ return [
|
|||
*/
|
||||
'auth_resolver' => null,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Identity Formatter
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Class used by admin tooling (`php artisan websockets:watch -v`) to render
|
||||
| the "User" column for each connection. The default formatter handles the
|
||||
| typical Eloquent User shape (id / name / username / email) and produces
|
||||
| strings like `#42 - Alice | alice42 - alice@example.com`, dropping any
|
||||
| suffix whose source field is missing.
|
||||
|
|
||||
| Apps with non-User auth subjects (multi-tenant company accounts, api
|
||||
| clients, etc.) can implement
|
||||
| `BlaxSoftware\LaravelWebSockets\Contracts\IdentityFormatter` and either
|
||||
| name the class here, or bind it directly:
|
||||
|
|
||||
| $this->app->bind(IdentityFormatter::class, MyFormatter::class);
|
||||
|
|
||||
| Leave null to use the package default.
|
||||
|
|
||||
*/
|
||||
'identity_formatter' => null,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Max Concurrent Children (Fork Limit)
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||
|
||||
namespace BlaxSoftware\LaravelWebSockets\Console\Commands;
|
||||
|
||||
use BlaxSoftware\LaravelWebSockets\Contracts\IdentityFormatter;
|
||||
use BlaxSoftware\LaravelWebSockets\Services\WebsocketService;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
|
|
@ -145,21 +146,20 @@ class WatchStats extends Command
|
|||
return '<fg=gray>Guest</>';
|
||||
}
|
||||
|
||||
$user = WebsocketService::getAuth($socketId);
|
||||
if (! $user) {
|
||||
// Authed but the per-socket user blob expired / was evicted.
|
||||
$subject = WebsocketService::getAuth($socketId);
|
||||
if (! $subject) {
|
||||
// Authed-but-expired: the socket is in ws_socket_authed_users
|
||||
// but the per-socket user blob has fallen out of cache. Show the
|
||||
// bare id from authedUsers so the connection isn't mislabeled
|
||||
// as a Guest.
|
||||
return '<fg=yellow>#' . $authedUsers[$socketId] . '</>';
|
||||
}
|
||||
|
||||
// Try common identity fields in order of usefulness. Fall back to the
|
||||
// user id so we always have something to display.
|
||||
$label = $user->name
|
||||
?? $user->display_name
|
||||
?? $user->username
|
||||
?? $user->email
|
||||
?? ('#' . ($user->id ?? '?'));
|
||||
|
||||
return (string) $label;
|
||||
// Delegate formatting to the bound IdentityFormatter so apps with
|
||||
// non-User subjects (Company, ApiClient, etc.) can render their own
|
||||
// shape without forking the package.
|
||||
$formatter = app(IdentityFormatter::class);
|
||||
return $formatter->format($subject, $socketId);
|
||||
}
|
||||
|
||||
private function formatDuration(string $socketId, int $now): string
|
||||
|
|
|
|||
|
|
@ -0,0 +1,45 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace BlaxSoftware\LaravelWebSockets\Contracts;
|
||||
|
||||
/**
|
||||
* Renders the "who" associated with a websocket connection for admin tooling
|
||||
* (`websockets:watch -v`, server-info dumps, debug logs).
|
||||
*
|
||||
* The package has no opinion on what kind of subject is authenticated against
|
||||
* a socket — most apps store an Eloquent User, but a multi-tenant app might
|
||||
* authenticate a Company, an api-only app might use an ApiClient, etc.
|
||||
* `WebsocketService::setUserAuthed()` accepts whatever object the app passes,
|
||||
* so the corresponding formatter is the app's responsibility too.
|
||||
*
|
||||
* The package ships a `DefaultIdentityFormatter` that handles the common
|
||||
* Eloquent-User shape (id / name / username / email). Apps that need
|
||||
* different fields, multiple subject types, or custom formatting can bind
|
||||
* their own implementation:
|
||||
*
|
||||
* // In an app service provider:
|
||||
* $this->app->bind(
|
||||
* \BlaxSoftware\LaravelWebSockets\Contracts\IdentityFormatter::class,
|
||||
* \App\Websockets\MyIdentityFormatter::class,
|
||||
* );
|
||||
*
|
||||
* Or via config (`config/websockets.php`):
|
||||
*
|
||||
* 'identity_formatter' => \App\Websockets\MyIdentityFormatter::class,
|
||||
*/
|
||||
interface IdentityFormatter
|
||||
{
|
||||
/**
|
||||
* Return a human-readable label for the given subject.
|
||||
*
|
||||
* @param mixed $subject Whatever was cached via setUserAuthed() — an
|
||||
* Eloquent model, a stdClass, null for guests,
|
||||
* or an arbitrary value the app stored.
|
||||
* @param string $socketId The socket id this subject is attached to.
|
||||
* Useful if the formatter wants to pull
|
||||
* additional context from elsewhere.
|
||||
*/
|
||||
public function format(mixed $subject, string $socketId): string;
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace BlaxSoftware\LaravelWebSockets\Identity;
|
||||
|
||||
use BlaxSoftware\LaravelWebSockets\Contracts\IdentityFormatter;
|
||||
|
||||
/**
|
||||
* Default identity formatter — handles the common case of an Eloquent User
|
||||
* (or any object exposing id / name / username / email properties).
|
||||
*
|
||||
* Output shape: `#<id> - <name> | <username> - <email>` with each suffix
|
||||
* dropped if the corresponding field is missing or null. Returns "Guest"
|
||||
* for null subjects and for subjects that have no usable id at all.
|
||||
*
|
||||
* Apps with a different subject shape (Company, ApiClient, custom auth blob)
|
||||
* should implement `IdentityFormatter` and bind it in their service provider
|
||||
* — see the interface docblock for an example.
|
||||
*/
|
||||
class DefaultIdentityFormatter implements IdentityFormatter
|
||||
{
|
||||
public function format(mixed $subject, string $socketId): string
|
||||
{
|
||||
if (! $subject) {
|
||||
return 'Guest';
|
||||
}
|
||||
|
||||
$id = $this->read($subject, 'id');
|
||||
if ($id === null || $id === '') {
|
||||
return 'Guest';
|
||||
}
|
||||
|
||||
$out = '#' . $id;
|
||||
|
||||
if ($name = $this->read($subject, 'name')) {
|
||||
$out .= ' - ' . $name;
|
||||
}
|
||||
if ($username = $this->read($subject, 'username')) {
|
||||
$out .= ' | ' . $username;
|
||||
}
|
||||
if ($email = $this->read($subject, 'email')) {
|
||||
$out .= ' - ' . $email;
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Best-effort property read across Eloquent models, stdClass, and arrays.
|
||||
*/
|
||||
private function read(mixed $subject, string $key): mixed
|
||||
{
|
||||
if (is_array($subject)) {
|
||||
return $subject[$key] ?? null;
|
||||
}
|
||||
if (is_object($subject)) {
|
||||
// Eloquent and stdClass both support property access; missing
|
||||
// attributes on Eloquent return null instead of throwing.
|
||||
return $subject->{$key} ?? null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -31,6 +31,7 @@ class WebSocketsServiceProvider extends ServiceProvider
|
|||
$this->registerWebSocketHandler();
|
||||
$this->registerRouter();
|
||||
$this->registerManagers();
|
||||
$this->registerIdentityFormatter();
|
||||
$this->registerBroadcastAuthRoute();
|
||||
$this->registerCommands();
|
||||
}
|
||||
|
|
@ -81,6 +82,30 @@ class WebSocketsServiceProvider extends ServiceProvider
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind the identity formatter used by admin tooling (`websockets:watch -v`).
|
||||
*
|
||||
* Resolution order:
|
||||
* 1. Whatever is already bound to `Contracts\IdentityFormatter` — apps
|
||||
* that called `$this->app->bind()` in their own provider win, because
|
||||
* this register runs in `boot()` after their `register()`.
|
||||
* 2. The class named in `config('websockets.identity_formatter')`.
|
||||
* 3. The package default (`DefaultIdentityFormatter`).
|
||||
*/
|
||||
protected function registerIdentityFormatter()
|
||||
{
|
||||
if ($this->app->bound(Contracts\IdentityFormatter::class)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$configured = config('websockets.identity_formatter');
|
||||
$class = is_string($configured) && $configured !== ''
|
||||
? $configured
|
||||
: Identity\DefaultIdentityFormatter::class;
|
||||
|
||||
$this->app->singleton(Contracts\IdentityFormatter::class, $class);
|
||||
}
|
||||
|
||||
protected function registerBroadcastAuthRoute()
|
||||
{
|
||||
if (! Route::has('broadcasting/auth')) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue