Creating and working with anonymous users in Laravel
Tags: laravel, php
Published: 2022-08-08 20:05:58

First off, I'm defining an "anonymous user" as the user who signs in via a "guest login". A one-tap way to create a new guest account and get in-game to try things out without pledging your email or committing a new password to memory.

With that out of the way...

---

A nagging issue I see in the PBBG (persistent browser-based game) space is games with little to no information on the home page other than a login and registration form.

New players want to know what your game is like! And **most** people are unwilling to fill out a registration form to get a peek at it.

The easiest (and most oft-requested) solution to start is adding screenshots to your home page. Show potential players what your game looks like. Explain the major features. Sell your game!

The next-best solution (in my opinion) is to add a guest login.

Allow your future players to tap a single button, get in-game, and get their feet wet. All with no commitment.

Sure, you could design a walkthrough or a tutorial. But most of the types of games we're creating interact with the database _very_ early on. While you can mock out some of the details by storing user data in the session (for example), I have found it much easier to just create a concrete `User` implementation. That way I can easily tie it to other concrete models like inventory systems and more.

Then, after they're killed their first few monsters or started their first kingdom or completed their first quest, let them know they need to register to save their progress! Players who are going to stick around will happily register, while those who don't like your game will fall off.

Some of the marketing benefits of this type of system:

1. You get a much lower bounce rate on your home page.
2. You get to design your onboarding process clearly and concretely for new players.
3. You get better data and metrics for how many players register, or where exactly they got stuck in-game.

So let's walk through how I designed the anonymous user functionality for Trounced (new kingdom-builder browser game) in Laravel.

### 1. Update your migrations

The `users` table that comes with a new Laravel installation requires that you have both the name and password fields filled out. We'll update those so they can be nullable.

```php
Schema::create('users', function (Blueprint $table) {
    $table->id();
    $table->string('name');
    $table->string('email')->nullable()->unique();
    $table->string('password')->nullable();
    // Your other table fields here...
});
```

The result being that you can now create users without any email or password at all.

Great!

### 2. Create a Guest Login controller

I created a new controller to handle all the guest logic in one place. The first method we're going to add is a one-click guest login. We'll add the registration method to it later.

```php
class GuestLoginController extends Controller
{
    public function login(): RedirectResponse
    {
        $randomNumbers = rand(pow(10, 5 - 1), pow(10, 5) - 1);
        $guest = User::create([
            'name' => "Guest{$randomNumbers}",
        ]);

        Auth::loginUsingId($guest->id);
        return redirect()->whereverYouWant();
    }
}
```

This lets you create a new user with a random 5-digit number after the name 'Guest'. Remember, we don't need any other fields to create a valid user.

Now wire up this route to a button on your home page. I'd also recommend sticking it behind some kind of [rate limiter](https://laravel.com/docs/master/routing#rate-limiting) so you don't get any bad actors abusing your system.

```php
// app/Providers/RouteServiceProvider.php

RateLimiter::for('guest-registration', function (Request $request) {
    return Limit::perMinute(2)->by($request->ip());
});
```

```php
// routes/web.php

Route::post('/guest-login', [GuestLoginController::class, 'login'])
    ->middleware(['throttle:guest-registration']);
```

### 3. Interacting with 'Guest' users

At some point we'll want to encourage our guests to register, but for now we will interact with them just as guests.

The easiest way to check if a user is a guest or not is by seeing if their `email` is `null`.

I have opted to put this as an attribute on the User model to easily tell if a user is a guest:

```php
// User Model
public function isGuest(): Attribute
{
    return new Attribute(
        get: fn ($v, $attr) => $attr['email'] === null,
    );
}
```

Now you can limit any functionality you want to keep from guest users.

On my front-end, I hide parts of the UI or show different messages to guest users depending on the `isGuest` attribute that I pass to the front end.

On my back-end I put specific checks in place to refuse certain functionality to guest users.

The implementation of this is now very straightforward and entirely up to you.

### 4. Encouraging registration

I tend toward being overly communicative to guest users (read: annoying).

I stick a big fat brightly-colored banner on every single page of the game that says "You're currently playing a Guest account. Click here to save your progress!". Clicking the banner takes them to the registration page.

You may opt to be a bit more subtle, or encourage guests to register after an initial tutorial.

This is where you might want to consider the marketing side of things a bit more. Do you want guests to convert to users no matter what? Do you prefer to discourage players from registering until they reach a certain point of the game? This is entirely up to you.

### 5. Guest Registration

Instead of creating a whole new user, we just want to convert the player's existing guest account into a fully-fledged account by having them give us a name, email and password.

In Trounced, clicking on the ~~annoying~~ persistent banner takes the guest to a registration page where we collect exactly that information from them via a form.

On the back-end, I validate this request and update the guest user. You could even throw in some additional rewards here if you wanted, as incentive to get guests to register their account.

```php
// GuestLoginController

public function store(StoreGuestRequest $request): RedirectResponse
{
    Auth::user()->name = $request->get('name');
    Auth::user()->email = $request->get('email');
    Auth::user()->password = Hash::make($request->get('password'));
    Auth::user()->save();

    return redirect()->backToTheGame();
}
```

Then in my `StoreGuestRequest`, I make sure I'm getting all the required info and only allowing guests to hit this endpoint.

```php
class StoreGuestRequest extends FormRequest
{
    public function authorize()
    {
        return Auth::user()->isGuest;
    }

    public function rules()
    {
        return [
            'name' => ['required', 'string', 'max:255'],
            'email' => ['required', 'email', 'max:255', Rule::unique('users')->ignore(Auth::id())],
            'password' => ['required', 'string', new Password, 'confirmed'],
        ];
    }
}
```

Now that your user has a valid email, the `isGuest` attribute will always return `false` and your game will function as normal. Full circle!

### 6. Cleaning up old Guest accounts

If someone comes and tries out your game but it's not for them, you might not want that `Guest69420` user hanging out in your database forever.

In that case, we'll make the `User` model [prunable](https://laravel.com/docs/master/eloquent#pruning-models), which is Laravel's built-in way of cleaning up old models.

We add the `Prunable` trait to the `User` model and then define a new `prunable` method to define the appropriate criteria. Check the documentation here, it describes all the setup.

In my case, any `User` models that still have a `null` email and were created 3 or more days ago should be deleted. Feel free to change the criteria, but remember that once a user loses authentication (such as session expiring) with your system, there's no way to re-authenticate them since there's no login information!

```php
// User Model
public function prunable()
{
    return static::whereNull('email')
        ->where('created_at', '<', now()->subDays(3));
}
```

Now, in our `app/Console/Kernel.php` we'll register the command to prune the model regularly:

```php
protected function schedule(Schedule $schedule)
{
    $schedule->command('model:prune', [
        '--model' => [User::class],
    ])->daily();
}
```

Be sure to also clean up any related models (such as inventories) so you don't have useless data floating around in the ether. This is typically best done in an observer, which is outside the scope of this post.

Now old Guest users are automatically cleaned up and thrown out of the database.

---

That's all, folks!

As with everything in programming, I'm sure there are other (better) ways to accomplish this same type of functionality. For me, I needed a simple way to get players in-game quickly without getting hung up on a registration form.

This bit of logic does exactly that, and it doesn't interfere with any other areas of my application or require me to write lots of abstract conditional logic.

Hopefully it can benefit you, too!