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,
|
'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)
|
| Max Concurrent Children (Fork Limit)
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace BlaxSoftware\LaravelWebSockets\Console\Commands;
|
namespace BlaxSoftware\LaravelWebSockets\Console\Commands;
|
||||||
|
|
||||||
|
use BlaxSoftware\LaravelWebSockets\Contracts\IdentityFormatter;
|
||||||
use BlaxSoftware\LaravelWebSockets\Services\WebsocketService;
|
use BlaxSoftware\LaravelWebSockets\Services\WebsocketService;
|
||||||
use Illuminate\Console\Command;
|
use Illuminate\Console\Command;
|
||||||
|
|
||||||
|
|
@ -145,21 +146,20 @@ class WatchStats extends Command
|
||||||
return '<fg=gray>Guest</>';
|
return '<fg=gray>Guest</>';
|
||||||
}
|
}
|
||||||
|
|
||||||
$user = WebsocketService::getAuth($socketId);
|
$subject = WebsocketService::getAuth($socketId);
|
||||||
if (! $user) {
|
if (! $subject) {
|
||||||
// Authed but the per-socket user blob expired / was evicted.
|
// 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] . '</>';
|
return '<fg=yellow>#' . $authedUsers[$socketId] . '</>';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try common identity fields in order of usefulness. Fall back to the
|
// Delegate formatting to the bound IdentityFormatter so apps with
|
||||||
// user id so we always have something to display.
|
// non-User subjects (Company, ApiClient, etc.) can render their own
|
||||||
$label = $user->name
|
// shape without forking the package.
|
||||||
?? $user->display_name
|
$formatter = app(IdentityFormatter::class);
|
||||||
?? $user->username
|
return $formatter->format($subject, $socketId);
|
||||||
?? $user->email
|
|
||||||
?? ('#' . ($user->id ?? '?'));
|
|
||||||
|
|
||||||
return (string) $label;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function formatDuration(string $socketId, int $now): string
|
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->registerWebSocketHandler();
|
||||||
$this->registerRouter();
|
$this->registerRouter();
|
||||||
$this->registerManagers();
|
$this->registerManagers();
|
||||||
|
$this->registerIdentityFormatter();
|
||||||
$this->registerBroadcastAuthRoute();
|
$this->registerBroadcastAuthRoute();
|
||||||
$this->registerCommands();
|
$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()
|
protected function registerBroadcastAuthRoute()
|
||||||
{
|
{
|
||||||
if (! Route::has('broadcasting/auth')) {
|
if (! Route::has('broadcasting/auth')) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue