jonhassall / belongs-to-user-or-guest
Trait for models that can belong to either a user or a guest session
Package info
github.com/jonhassall/BelongsToUserOrGuest
pkg:composer/jonhassall/belongs-to-user-or-guest
Requires
- illuminate/database: ^9.0|^10.0|^11.0|^12.0|^13.0
- illuminate/http: ^9.0|^10.0|^11.0|^12.0|^13.0
- illuminate/support: ^9.0|^10.0|^11.0|^12.0|^13.0
Requires (Dev)
- orchestra/testbench: ^7.0|^8.0|^9.0|^10.0|^11.0
- phpunit/phpunit: ^9.6|^10.5|^11.0
This package is auto-updated.
Last update: 2026-08-14 00:08:05 UTC
README
A Laravel package that provides traits for Eloquent models to support ownership by either authenticated users or guest sessions. This is perfect for applications where you want to allow both logged-in users and guests to interact with your application - for example, shopping carts, wishlists, favorites, or any feature that should work without requiring users to sign up first.
Features
BelongsToUserOrGuest Trait
Attach this trait to any model that can belong to either a logged-in user or a guest session.
Key Features:
- Automatic Assignment: Automatically assign models to the current user or guest session
- Ownership Check: Check if a model belongs to the current request
- Query Scopes: Retrieve only models owned by the current user or guest
- Guest-to-User Migration: Move guest-owned models to a user account when they log in
- Flexible Relationships: Support for custom guest model classes and foreign keys
IsGuest Trait
Attach this trait to your guest session model to manage guest sessions with cookie-based tracking.
Key Features:
- Cookie Handling: Persistent cookies to track guests across visits
- IP Tracking: Optional IP address storage for security or analytics
- Binary Token Storage: Optionally store session tokens as compact 32-byte values
- Session Cleanup: Remove old, expired guest sessions
- Caching: Request-level caching for performance
- Configurable: Customizable cookie names, durations, and behaviors
Requirements
- Laravel: 9.x through 13.x
- PHP: 8.1 or higher
- Cookies: This package uses cookies to track guest sessions and does not require Laravel's session driver to be configured. All guest session management is handled entirely through cookies, making it lightweight and independent of your application's session configuration.
Installation
Option 1: Via Packagist (Recommended)
Install the package via Composer:
composer require jonhassall/belongs-to-user-or-guest
Option 2: Install Directly from GitHub
You can install it directly from the GitHub repository. Add this to your composer.json:
{
"repositories": [
{
"type": "vcs",
"url": "https://github.com/jonhassall/BelongsToUserOrGuest.git"
}
],
"require": {
"jonhassall/belongs-to-user-or-guest": "dev-main"
}
}
Then run:
composer install
Quick Start
Step 1: Set Up Your Guest Model
Create a migration for your guest sessions table:
php artisan make:migration create_guests_table
Schema::create('guests', function (Blueprint $table) { $table->id(); $table->string('session_token', 64)->unique(); // Guest session token (not Laravel session) $table->string('ip_address')->nullable(); // Optional $table->timestamps(); });
Create your Guest model:
namespace App\Models; use Illuminate\Database\Eloquent\Model; use JonHassall\BelongsToUserOrGuest\IsGuest; class Guest extends Model { use IsGuest; protected $fillable = ['session_token', 'ip_address']; // Required: Cookie name for guest sessions protected function getCookieName(): string { return 'guest_session'; } // Optional: Cookie duration in minutes (default: 1 year) protected function getCookieDuration(): int { return 525600; // 1 year } // Optional: Store guest IP addresses protected function shouldStoreIP(): bool { return true; } // Optional: Store session tokens as 32 raw bytes rather than hexadecimal text. // Requires the binary guest-table schema shown below. protected function shouldStoreSessionTokenAsBinary(): bool { return true; } }
Step 2: Set Up Your Resource Model
Add the necessary columns to your resource table:
Schema::create('favorites', function (Blueprint $table) { $table->id(); $table->foreignId('user_id')->nullable()->constrained()->onDelete('cascade'); $table->foreignId('guest_id')->nullable()->constrained()->onDelete('cascade'); $table->string('item_name'); $table->timestamps(); });
Add the trait to your model:
namespace App\Models; use Illuminate\Database\Eloquent\Model; use JonHassall\BelongsToUserOrGuest\BelongsToUserOrGuest; class Favorite extends Model { use BelongsToUserOrGuest; protected $fillable = ['item_name', 'user_id', 'guest_id']; // Required: Specify your guest model class protected function getGuestModel(): string { return Guest::class; } // Optional: Custom foreign key (default: 'guest_id') protected function getGuestForeignKey(): string { return 'guest_id'; } }
Usage Examples
Creating Records with Ownership
// Method 1: Create and assign manually $favorite = new Favorite(['item_name' => 'Laravel']); $favorite->assignToCurrentOwner(); // Automatically assigns to user or guest // Method 2: Create with ownership in one step $favorite = Favorite::createWithOwnership([ 'item_name' => 'Laravel' ]); // Method 3: Update or create with ownership $favorite = Favorite::updateOrCreateWithOwnership( ['item_name' => 'Laravel'], // Search criteria ['updated_at' => now()] // Additional values );
Querying Records
// Get all favorites for current user or guest $favorites = Favorite::forCurrentOwner()->get(); // Check if a specific favorite belongs to current user/guest if ($favorite->isOwnedByCurrentRequest()) { // Allow editing or deletion } // Access the owner (returns User or Guest model) $owner = $favorite->owner;
Migrating Guest Data to User Account
When a guest logs in or signs up, transfer their data:
// Option 1: In your login/registration controller use App\Models\Favorite; use App\Models\Guest; public function login(Request $request) { // After successful authentication... // Move all guest favorites to the logged-in user Favorite::moveGuestToUser($request); // Optionally delete the guest session Guest::deleteGuestSession($request); }
// Option 2: Automatically on login using AppServiceProvider // In app/Providers/AppServiceProvider.php use Illuminate\Support\Facades\Event; use Illuminate\Auth\Events\Login; use App\Models\Favorite; use App\Models\ShoppingCart; use App\Models\Guest; public function boot(): void { // Automatically move guest data when user logs in Event::listen(function (Login $event) { // Move guest-owned models to the logged-in user Favorite::moveGuestToUser(); // Delete the guest session after migrating data Guest::deleteGuestSession(); // Optional: Notify the user their data was saved if (request()->hasSession()) { session()->flash('success', 'Your favorites and cart have been saved to your account.'); } }); }
Working with Guest Sessions Directly
use App\Models\Guest; // Get or create guest for current request $guest = Guest::getOrCreateGuest(); // Check if current request is a guest (not logged in) if (Guest::isGuest()) { // Show guest-specific messaging } // Get current user or guest $currentOwner = Guest::getCurrentUserOrGuest(); // Clean up old guest sessions (run in scheduled task) $deletedCount = Guest::cleanup();
Advanced Usage
// Create without auto-saving $favorite = new Favorite(['item_name' => 'Vue']); $favorite->assignToCurrentOwner(persist: false); // Don't save yet $favorite->some_other_field = 'value'; $favorite->save(); // Save when ready // Remove ownership $favorite->removeOwnership(); // Clears both user_id and guest_id // Query scopes with explicit request $favorites = Favorite::forCurrentOwner($customRequest)->get();
Method Reference
BelongsToUserOrGuest Methods
| Method | Description |
|---|---|
assignToCurrentOwner(?Request $request, bool $persist = true) |
Assign model to current user or guest |
removeOwnership(bool $persist = true) |
Clear ownership from model |
isOwnedByCurrentRequest(?Request $request) |
Check if current user/guest owns this model |
scopeForCurrentOwner($query, ?Request $request) |
Query scope for current owner's models |
moveGuestToUser(?Request $request) |
Transfer all guest models to logged-in user |
createWithOwnership(array $data, ?Request $request) |
Create model with ownership in one step |
updateOrCreateWithOwnership(array $attributes, array $values, ?Request $request) |
Update or create with ownership |
user() |
Relationship to User model |
guest() |
Relationship to Guest model |
owner |
Attribute accessor for current owner (User or Guest) |
IsGuest Methods
| Method | Description |
|---|---|
getOrCreateGuest(?Request $request, bool $createIfNeeded = true) |
Get or create guest session |
getGuest(?Request $request, bool $createIfNeeded, bool $ignoreLoggedIn) |
Get guest session with options |
getCurrentUserOrGuest(?Request $request, bool $createIfNeeded) |
Get authenticated user or guest |
isGuest(?Request $request) |
Check if request is from a guest |
deleteGuestSession(?Request $request) |
Delete guest session and cookie |
cleanup() |
Remove expired guest sessions |
Configuration
BelongsToUserOrGuest Configuration
| Method | Description |
|---|---|
getGuestModel(): string |
Return the fully qualified class name of your guest model. (Required) |
getGuestForeignKey(): string |
Foreign-key column name; defaults to 'guest_id'. (Optional) |
IsGuest Configuration
| Method | Description |
|---|---|
getCookieName(): string |
Return the cookie name for guest sessions. (Required) |
getCookieDuration(): int |
Cookie lifetime in minutes; defaults to 525600 (one year). (Optional) |
shouldStoreIP(): bool |
Enable IP address storage; defaults to false. (Optional) |
shouldStoreSessionTokenAsBinary(): bool |
Store session tokens as 32 raw bytes rather than 64-character hexadecimal text; model attributes and cookies remain hexadecimal text. Defaults to false; see the binary schema option below. (Optional) |
The package generates a cryptographically secure 64-character hexadecimal token whenever it creates a guest session.
Database Schema Requirements
Resource Table (e.g., favorites)
$table->foreignId('user_id')->nullable()->constrained()->onDelete('cascade'); $table->foreignId('guest_id')->nullable()->constrained('guests')->onDelete('cascade');
Guest Table
$table->id(); // Default: 64-character hexadecimal text storage. $table->string('session_token', 64)->unique(); // Binary token option for MySQL: // - Stores 32 raw bytes instead of 64 bytes of hexadecimal text (50% less token storage). // - The unique index is smaller, which can improve index-cache efficiency for large tables. // - The model attribute and cookie remain hexadecimal text. // - Replace the string() line above with this only after legacy guest rows are migrated or expired: // $table->char('session_token', 32)->charset('binary')->unique(); // BINARY(32) $table->string('ip_address')->nullable(); // If using shouldStoreIP() $table->timestamps();
Scheduled Tasks
Add to app/Console/Kernel.php to clean up old guest sessions:
protected function schedule(Schedule $schedule) { $schedule->call(function () { \App\Models\Guest::cleanup(); })->daily(); }
Migrating an Existing MySQL Guest Table to Binary Tokens
Keep shouldStoreSessionTokenAsBinary() returning false while this migration runs. The following reversible Laravel migration preserves existing 64-character hexadecimal tokens by converting them through a temporary binary column. It assumes Laravel's default guests_session_token_unique index name; adjust the table or index names for a custom schema. Always back up your database before migrating!
use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Schema; return new class extends Migration { public function up(): void { // Add a nullable binary column while the legacy text token remains in use. Schema::table('guests', function (Blueprint $table): void { // MySQL treats CHAR with the binary character set as binary string data. $table->char('session_token_binary', 32)->charset('binary')->nullable(); }); // Convert every existing hexadecimal token in one database-side update. DB::table('guests')->update([ 'session_token_binary' => DB::raw('UNHEX(session_token)'), ]); // Remove the old text column and its unique index after every row is converted. Schema::table('guests', function (Blueprint $table): void { $table->dropUnique(['session_token']); $table->dropColumn('session_token'); }); // Give the converted binary column the package's expected column name. Schema::table('guests', function (Blueprint $table): void { $table->renameColumn('session_token_binary', 'session_token'); }); // Restore the non-null constraint and unique index from the original schema. Schema::table('guests', function (Blueprint $table): void { $table->char('session_token', 32)->charset('binary')->nullable(false)->change(); $table->unique('session_token'); }); } public function down(): void { // Add a nullable text column while the binary token remains in use. Schema::table('guests', function (Blueprint $table): void { $table->string('session_token_text', 64)->nullable(); }); // Convert every binary token back to lowercase hexadecimal text in one update. DB::table('guests')->update([ 'session_token_text' => DB::raw('LOWER(HEX(session_token))'), ]); // Remove the binary column and its unique index after every row is converted. Schema::table('guests', function (Blueprint $table): void { $table->dropUnique(['session_token']); $table->dropColumn('session_token'); }); // Restore the original text column name. Schema::table('guests', function (Blueprint $table): void { $table->renameColumn('session_token_text', 'session_token'); }); // Restore the non-null constraint and unique index from the original schema. Schema::table('guests', function (Blueprint $table): void { $table->string('session_token', 64)->nullable(false)->change(); $table->unique('session_token'); }); } };
Enable shouldStoreSessionTokenAsBinary() only after the migration succeeds. Before rolling back, coordinate the rollback with changing that method back to false. Run either direction in a maintenance window so requests never use the text-storage mode against the binary column, or vice versa. The cookie remains hexadecimal text, so converted sessions continue to resolve. Take a database backup first. Laravel 9 may require doctrine/dbal for the column rename or change() operation; PostgreSQL and SQLite need the same temporary-column, backfill, and swap sequence using their native binary type.
Versioning and Branches
- Stable releases use semantic version tags, for example:
v1.0.2 - The
mainbranch is the primary release branch - Composer branch alias maps
dev-mainto1.x-devfor users tracking development - Patch releases should branch from
main, then be tagged and pushed
Development Branches
Develop unreleased work on a development/<feature> branch, validate it there, then merge it into main when it is ready for a public release. Do not tag or publish from a development branch.
git switch main
git pull origin main
git switch -c development/binary-session-token-storage
composer test
Release example:
git checkout main
git pull origin main
git tag -a v1.0.3 -m "Release v1.0.3"
git push origin main --tags
Full release checklist: RELEASE_CHECKLIST.md
Testing
composer install
composer test
License
This package is open-sourced software licensed under the MIT license.