Category Archives: PHP

Impersonate users with Sanctum in Laravel

I needed to write a somewhat clean solution to let an admin impersonate other users. Which basically means that one user can appear as another user – without having to get access that users credentials to log in.

The most obvious use case for this would be when an admin needs access to a user’s account. The app I did this for relies heavily on user created data, so this is very useful when customer service needs to look into any problems the users may have. 

Earlier we were using Tymon JWT for authentication and it was quite easy to implement impersonation it with Rickycezar/laravel-jwt-impersonate

But after migrating to Sanctum for authentication I needed to come up with something else. Here’s my solution (I posted this first in a stackoverflow thread).

How it works

In front end admins can view a list of all users, where they can klick a button to triggers the impersonate endpoint for any user. After doing this the admin will appear to the system as the impersonated user.

When getting the response from the endpoint, front end sets a switch to keep track of that the current user is an impersonation – and to shows a button to leave impersonation. In essence what happens in front end is just that the access token is replaced when starting and ending an impersonation. 

In backend what what happens is:
Impersonate
1. Create a new access token for the impersonated user
2. Save a connection between this impersonation and the admin user 
3. Delete the admin’s access token
4. Send the new access token back

Leave impersonation
1. Create a new access token for the admin connected to the impersonation token
2. Delete the impersonation token
3. Send the new access token back

It’s quite simple. So here we go: 

1. Migration

First we write a migration that will create a new table called impersonations. This table will store the connection between a personal access token and the impersonating user.

public function up()
{
    Schema::create('impersonations', function (Blueprint $table) {
        $table->id();
        $table->bigInteger('personal_access_token_id')->unsigned();
        $table->bigInteger('user_id')->unsigned();
        $table->timestamps();

        $table->foreign('personal_access_token_id')->references('id')->on('personal_access_tokens')->cascadeOnDelete();
        $table->foreign('user_id')->references('id')->on('users')->cascadeOnDelete();
    });
}

2. User model

Add three function to your USER model. These functions are used to determine if a user can impersonate others, can be impersonated by others and if the user is currently impersonating. You can make up your own rules for who can impersonate or get impersonated – just add whatever logic you want to the canImpersonate and canBeImpersonated functions.

I chose to only let admins impersonate and only non admins to be impersonated.

public function canImpersonate()
{
    return $this->is_admin;
}


public function canBeImpersonated()
{
    return !$this->is_admin;
}


public function isImpersonated() {
    $token = $this->currentAccessToken();
    return $token->name == 'IMPERSONATION token';
}

3. Impersonation Controller Functions

Add two functions. One to start impersonation (take the persona of another user), and one function to leave impersonation (go back to your own user).

In my case i have an AdminController. But you can put these functions wherever it makes sense to you. Maybe you want a dedicated ImpersonationController.

In these function there are two users – the impersonator and the persona. The impersonator is the admin who wants to access the system as another user, and the persona is the user that’s being impersonated.

Also you can modify the responses to however you prefer it for your frontend app. You most likely want to keep track of whether the user is actually impersonating another user, so you can add button for the admin to go back to its own account. I used the same structure as Rickycezar/laravel-jwt-impersonate so the we didn’t have to make any changes in front end from our old solution.

// START IMPERSONATION
public function impersonate($userId)
{
    $impersonator = auth()->user();
    $persona = User::find($userId);

    // Check if persona user exists, can be impersonated and if the impersonator has the right to do so.
    if (!$persona || !$persona->canBeImpersonated() || !$impersonator->canImpersonate()) {
        return false;
    }

    // Create new token for persona
    $personaToken = $persona->createToken('IMPERSONATION token');

    // Save impersonator and persona token references
    $impersonation = new Impersonation();
    $impersonation->user_id = $impersonator->id;
    $impersonation->personal_access_token_id = $personaToken->accessToken->id;
    $impersonation->save();

    // Log out impersonator
    $impersonator->currentAccessToken()->delete();

    $response = [
        "requested_id" => $userId,
        "persona" => $persona,
        "impersonator" => $impersonator,
        "token" => $personaToken->plainTextToken
    ];

    return response()->json(['data' => $response], 200);
}


// LEAVE IMPERSONATION
public function leaveImpersonate()
{
    // Get impersonated user
    $impersonatedUser = auth()->user();

    // Find the impersonating user
    $currentAccessToken = $impersonatedUser->currentAccessToken();
    $impersonation = Impersonation::where('personal_access_token_id', $currentAccessToken->id)->first();
    $impersonator = User::find($impersonation->user_id);
    $impersonatorToken = $impersonator->createToken('API token')->plainTextToken;

    // Logout impersonated user
    $impersonatedUser->currentAccessToken()->delete();

    $response = [
        "requested_id" => $impersonator->id,
        "persona" => $impersonator,
        "token" => $impersonatorToken,
    ];

    return response()->json(['data' => $response], 200);
}

4. Routes

Last and least, the routes. Remember that if you’re using a middleware to protect the impersonate route, so only admins can access it, you need to put the leave impersonation route outside that middleware. Since the persona taken by the admin most likely wont be a admin

// Impersonate
$api->get('/impersonate/take/{userId}', [AdminController::class, 'impersonate'])->name('users.impersonate');

// Leave impersonation
 $api->get('/impersonate/leave', [AdminController::class, 'leaveImpersonate'])->name('users.leaveImpersonate');

(PHP) Sort array of objects by property value

usort($myArray, function($a, $b)
{
    return strcmp($a->myPropery, $b->myPropery);
});

Sort by predefined order:

$predefinedOrder = array(1,5,2,6);

usort($arrayOfObjects, function ($a, $b) use ($predefinedOrder ) {

    $flipped = array_flip($predefinedOrder);
    $left = $flipped[$a->myPropery)];
    $right = $flipped[$b->myPropery)];
    return $left >= $right;
});

(PHP) Add column to table in Laravel

Create migration in console:

php artisan make:migration add_mycolumn_to_mytable

Use Schema::table() to access existing table (instead of Schema::create() for creating new tables)

public function up()
{
    Schema::table('mytable', function($table) {
        $table->text('mycolumn');
    });
}
public function down()
{
    Schema::table('mytable', function($table) {
        $table->dropColumn('mycolumn');
    });
}

Then run migrations:

php artisan migrate

(PHP) Get a list of run database queries in Laravel

This is great if you want to see what queries are actually run when using Eloquent.

        // Get all querys run
        $queries = DB::getQueryLog(); 

        // If you want to sort them by time this works
        usort($queries, function ($a, $b) {
            return $a['time'] < $b['time'];
        });

        // Print them on screen in a readable way
        echo '<pre>';
        print_r($queries);
        echo '</pre>';

(PHP) Log Laravel execution time to console

Put this in app/start/global.php to get Laravels execution time to the browser console log.

L4

$start = microtime(true);

App::finish(function() use ($start) {
    echo "<script>console.log('App finish: ".round((microtime(true) - $start) * 1000, 3)." ms')</script>";
});

This works with L5:

This page took {{ (microtime(true) - LARAVEL_START) }} seconds to render

(PHP) Allow any delimiter in CSV file

In a Laravel app I made the users would upload CSV files. The problem was that fgetcsv only allows a single delimiter character, and it needs to be defined.

The problem with that is that when a user exports a CSV from MS Excel – it could end up using a wide array of delimiters depending on the locality settings in windows.  It kind of sucks that MS won’t let the user choose this on export, but that’s how it is.

So I solved it the quick and easy way, by making a function that simply replaces any delimiter character to  my desired delimiter. (Note that the tab character is in double quotes, since it won’t be interpreted as tab otherwise):

    /**
     * Will replace a number of CSV delimiters to one specific character
     * @param $file     CSV file
     */
    private function replaceDelimiters($file)
    {
        // Delimiters to be replaced: pipe, comma, semicolon, caret, tabs
        $delimiters = array('|', ';', '^', "\t");
        $delimiter = ',';

        $str = file_get_contents($file);
        $str = str_replace($delimiters, $delimiter, $str);
        file_put_contents($file, $str);
    }