Playa gives a Laravel app a lightweight Player model for visitors who are not necessarily logged in.
It is useful for QR-code journeys, event games, voting screens, kiosk flows, demos, and other places where you want to remember "this device came back" without making somebody create an account first.
The package stores the player in your database and keeps only the player's UUID in an HttpOnly cookie.
Install the package with Composer:
composer require charlielangridge/playaPublish and run the migration:
php artisan vendor:publish --tag="playa-migrations"
php artisan migratePublish the config if you want to change the cookie name, lifetime, table name, or user-linking behaviour:
php artisan vendor:publish --tag="playa-config"Add the playa middleware to any route that should have a temporary player.
use Illuminate\Support\Facades\Route;
Route::get('/join/{game}', JoinGameController::class)
->middleware(['web', 'playa'])
->name('games.join');When a visitor opens that URL, Playa will:
- create a
playa_playersrow if the device has no valid player cookie - store the player's UUID in a cookie
- resolve the same player on later visits
- refresh
last_seen_at - refresh
expires_atwhen renewal is enabled - make the player available from the request and the facade
This works well for direct links and QR codes. The QR code can point at a normal application route such as:
https://example.com/join/summer-party
The route handles your application flow; Playa handles the device identity.
From a controller or route closure:
use Illuminate\Http\Request;
public function __invoke(Request $request)
{
$player = $request->player();
// $player is an instance of CharlieLangridge\Playa\Models\Player
}From somewhere that does not receive the request:
use CharlieLangridge\Playa\Facades\Playa;
$player = Playa::player();You can also resolve or create players directly:
use CharlieLangridge\Playa\Facades\Playa;
$player = Playa::findByUuid($uuid);
$player = Playa::create([
'name' => 'Sam',
'username' => 'sam-27',
]);Read an existing, active cookie identity without creating, renewing, or attaching it to the request:
$player = Playa::findExisting($request);Choose a persistence policy when identity should only be created after application validation succeeds:
use CharlieLangridge\Playa\IdentityPolicy;
$player = Playa::resolve($request, IdentityPolicy::Session);
$player = Playa::resolve($request, IdentityPolicy::Rolling);Session identities use a browser-session cookie and keep their original server expiry. Rolling identities use the configured persistent lifetime and renew on visits. Requesting Rolling upgrades an existing Session identity without changing its UUID; requesting Session never downgrades an existing Rolling identity.
The Player model has first-class name and username columns, plus a data JSON column for application-specific details.
$player = request()->player();
$player->update([
'name' => 'Sam',
'username' => 'sam-27',
'data' => [
'team' => 'blue',
'accepted_rules_at' => now()->toIso8601String(),
],
]);Playa does not ship profile routes or forms. Build those in your application so validation, copy, and consent match the flow you are building.
If your application needs player-specific relationships, casts, scopes, or helpers, create an application model that extends Playa's model:
namespace App\Models;
use CharlieLangridge\Playa\Models\Player as PlayaPlayer;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Player extends PlayaPlayer
{
public function team(): BelongsTo
{
return $this->belongsTo(Team::class);
}
}Then configure Playa to use it:
'player_model' => App\Models\Player::class,The configured model must extend CharlieLangridge\Playa\Models\Player. Add any extra columns, such as team_id, in your application's own migrations.
Players can be linked to your app's user model without becoming authenticated users themselves.
$player = request()->player();
$player->linkUser(auth()->user());To remove the link:
$player->unlinkUser();Automatic linking is disabled by default. If you want any authenticated request with the playa middleware to claim the current player, enable it in config/playa.php:
'auto_link_authenticated_user' => true,By default, a player lasts for 30 days:
'lifetime_minutes' => 60 * 24 * 30,Renewal is enabled by default:
'renew_on_visit' => true,With renewal enabled, each visit through the playa middleware refreshes the database expires_at value and the cookie lifetime.
With renewal disabled, the player expires at the original expires_at time even if the device keeps visiting.
When a cookie points to an expired, missing, or invalid player, Playa creates a fresh player and sends a replacement cookie.
Expired rows are not deleted automatically. Schedule the prune command in your application if you want to clean them up:
use Illuminate\Support\Facades\Schedule;
Schedule::command('playa:prune')->daily();You can keep recently expired rows around for a grace period:
php artisan playa:prune --hours=24The published config looks like this:
return [
'table_name' => 'playa_players',
'player_model' => CharlieLangridge\Playa\Models\Player::class,
'lifetime_minutes' => 60 * 24 * 30,
'renew_on_visit' => true,
'default_policy' => 'rolling',
'policies' => [
'session' => [
'lifetime_minutes' => 60 * 24,
'renew_on_visit' => false,
'cookie_lifetime_minutes' => 0,
],
'rolling' => [],
],
'cookie' => [
'name' => 'playa_player',
'path' => '/',
'domain' => null,
'secure' => null,
'http_only' => true,
'same_site' => 'lax',
'lifetime_minutes' => null,
],
'user_model' => null,
'user_id_type' => 'bigint',
'auto_link_authenticated_user' => false,
];The default table is playa_players to avoid colliding with application tables that already use players.
Change this before publishing/running the migration:
'table_name' => 'players',The cookie stores the player UUID only. By default it is HttpOnly and SameSite Lax.
Set secure to true in production if your app always runs over HTTPS:
'cookie' => [
'secure' => true,
],If cookie.lifetime_minutes is null, Playa uses lifetime_minutes for the cookie as well.
The migration supports the common Laravel user key types:
'user_id_type' => 'bigint', // bigint, uuid, ulid, or stringSet this before publishing/running the migration.
If user_model is null, Playa falls back to config('auth.providers.users.model').
Playa dispatches events for the main lifecycle points:
CharlieLangridge\Playa\Events\PlayerCreatedCharlieLangridge\Playa\Events\PlayerResolvedCharlieLangridge\Playa\Events\PlayerRenewedCharlieLangridge\Playa\Events\PlayerExpiredCharlieLangridge\Playa\Events\PlayerLinkedToUser
For example:
use CharlieLangridge\Playa\Events\PlayerCreated;
use Illuminate\Support\Facades\Event;
Event::listen(PlayerCreated::class, function (PlayerCreated $event) {
// $event->player
});If your application needs to clear the device identity, call:
use CharlieLangridge\Playa\Facades\Playa;
Playa::forget();When called during a request using the playa middleware, the response will clear the player cookie.
Make sure the route uses the playa middleware.
Route::get('/join/{game}', JoinGameController::class)
->middleware(['web', 'playa']);Check that the browser is receiving and returning the configured cookie. Common causes are a mismatched cookie domain, a path that does not include the route being visited, or secure being enabled on a non-HTTPS local site.
Use the middleware on web routes so Laravel's cookie middleware can decrypt incoming cookies and encrypt outgoing ones.
Manual linking is the default:
request()->player()->linkUser(auth()->user());If you want automatic linking, enable auto_link_authenticated_user and make sure the request is already authenticated before the playa middleware runs.
Run the test suite:
composer testRun static analysis:
composer analyseFormat the code:
composer formatPlaya is an open-source Laravel package created and maintained by Ganda Studio.
Read the Ganda Studio case study.
Please see CHANGELOG for more information on what has changed recently.
The MIT License (MIT). Please see License File for more information.