Skip to content

v2.0

Choose a tag to compare

@franbarbalopez franbarbalopez released this 28 Jun 11:13
· 1 commit to 2.x since this release
a2d4d99

Upgrading from 1.x

Mirror 2.x is a major redesign of the public API. Applications upgrading from 1.x must update their user models, impersonation calls, exception handling, middleware, configuration, redirects, and event listeners.

Important

Mirror 2.x only supports Laravel authentication guards backed by the session driver. Token, API, and stateless guards are rejected.

Requirements

The runtime requirements remain:

  • PHP 8.2 or later
  • Laravel 11, 12, or 13

Update the dependency in your application and then refresh the published configuration:

composer require franbarbalopez/mirror:^2.0
php artisan vendor:publish --tag=mirror --force

Warning

Review your existing config/mirror.php before overwriting it. Several 1.x options no longer exist.

Upgrade checklist

  • Replace the Mirror\Concerns\Impersonatable trait with the Mirror\Contracts\Impersonatable contract.
  • Implement both canImpersonate() and canBeImpersonated() explicitly.
  • Replace Mirror::start() and its aliases with Mirror::impersonate().
  • Replace Mirror::stop() or Mirror::forceStop() with Mirror::leave().
  • Replace state-reading aliases with the new facade methods.
  • Move redirect behavior into your controllers or middleware.
  • Replace the removed Mirror middleware aliases.
  • Update exception imports and catch phase interfaces where appropriate.
  • Update event listeners to use $context instead of $guardName.
  • Update config/mirror.php.
  • Confirm every impersonator and target guard uses the Laravel session driver.
  • Test guard resolution when multiple guards use the same model.

1. Models must implement Impersonatable

1.x

In 1.x, applications could use a trait that supplied permissive defaults:

use Illuminate\Foundation\Auth\User as Authenticatable;
use Mirror\Concerns\Impersonatable;

class User extends Authenticatable
{
    use Impersonatable;

    public function canImpersonate(): bool
    {
        return $this->hasRole('admin');
    }
}

The trait returned true by default when capability methods were not overridden.

2.x

The trait has been removed. Every model that can initiate or receive impersonation must implement the contract:

use Illuminate\Foundation\Auth\User as Authenticatable;
use Mirror\Contracts\Impersonatable;

class User extends Authenticatable implements Impersonatable
{
    public function canImpersonate(): bool
    {
        return $this->hasRole('admin');
    }

    public function canBeImpersonated(): bool
    {
        return ! $this->hasRole('super-admin');
    }
}

Caution

There are no permissive default implementations in 2.x. Both methods are part of the model's explicit authorization boundary.

2. Starting impersonation

The start API has been consolidated into one method.

Method replacements

1.x 2.x
Mirror::start($user) Mirror::impersonate($user)
Mirror::as($user) Mirror::impersonate($user)
Mirror::startByKey($key) Resolve the model in your application, then call Mirror::impersonate($user)
Mirror::startByEmail($email) Resolve the model in your application, then call Mirror::impersonate($user)

Before

$redirectUrl = Mirror::start(
    user: $user,
    leaveRedirectUrl: route('admin.users.index'),
    startRedirectUrl: route('dashboard'),
);

return redirect($redirectUrl);

After

Mirror::impersonate(
    target: $user,
    guard: 'web',
    context: [
        'reason' => request('reason'),
        'ticket_id' => request('ticket_id'),
    ],
);

return redirect()->route('dashboard');

Mirror::impersonate() returns void. Mirror no longer chooses or returns redirect URLs.

Model lookup is now application-owned

The startByKey() and startByEmail() convenience methods were removed. Perform lookups using your application's model and query rules:

$user = User::query()->findOrFail($id);

Mirror::impersonate($user);
$user = User::query()
    ->where('email', $email)
    ->firstOrFail();

Mirror::impersonate($user);

This makes tenant scopes, soft-deletion rules, custom identifiers, and authorization behavior explicit in application code.

3. Leaving impersonation

Method replacements

1.x 2.x
Mirror::stop() Mirror::leave()
Mirror::forceStop() Removed
Mirror::leave() alias returning void Mirror::leave() returning the signed context array

Before

Mirror::stop();

return redirect(Mirror::getLeaveRedirectUrl());

After

$context = Mirror::leave();

audit('Impersonation ended', $context);

return redirect()->route('admin.users.index');

The leave redirect URL is no longer stored or exposed by Mirror. Your application must decide where to redirect.

forceStop() has no direct replacement. Expiration is no longer enforced inside the leave operation, so a separate bypass method is unnecessary.

4. Reading impersonation state

The public state API has been renamed and simplified.

1.x 2.x
Mirror::isImpersonating() Mirror::active()
Mirror::impersonating() Mirror::active()
Mirror::getImpersonator() Mirror::impersonator()
Mirror::impersonator() Mirror::impersonator()
Mirror::impersonatorId() Removed; use Mirror::impersonator()?->getAuthIdentifier()
Mirror::getLeaveRedirectUrl() Removed
No equivalent Mirror::impersonated()
No equivalent Mirror::expired()
No equivalent Mirror::context()

Example:

if (Mirror::active()) {
    $impersonator = Mirror::impersonator();
    $impersonated = Mirror::impersonated();
    $context = Mirror::context();
}

5. Expiration no longer performs HTTP behavior

In 1.x, the package included TTL middleware that could stop impersonation and redirect automatically.

In 2.x, Mirror::expired() only reports whether the configured TTL has elapsed. It does not log anyone out, redirect, or abort the request.

Create application middleware when automatic enforcement is required:

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Mirror\Facades\Mirror;
use Symfony\Component\HttpFoundation\Response;

class LeaveExpiredImpersonation
{
    public function handle(Request $request, Closure $next): Response
    {
        if (Mirror::active() && Mirror::expired()) {
            Mirror::leave();

            return redirect()
                ->route('admin.users.index')
                ->with('warning', 'Impersonation expired.');
        }

        return $next($request);
    }
}

6. Mirror middleware has been removed

The following 1.x middleware classes and aliases no longer exist:

Removed alias Removed class
mirror.ttl Mirror\Http\Middleware\CheckImpersonationTtl
mirror.require Mirror\Http\Middleware\RequireImpersonation
mirror.prevent Mirror\Http\Middleware\PreventImpersonation

Replace them with application middleware based on Mirror::active() and Mirror::expired().

Require active impersonation

if (! Mirror::active()) {
    abort(403);
}

Prevent access during impersonation

if (Mirror::active()) {
    abort(403);
}

Keeping these policies in the application lets you choose redirects, status codes, messages, route exclusions, and audit behavior.

7. Guard resolution changed

Mirror 2.x distinguishes between the impersonator guard and target guard.

The impersonator guard is the first authenticated Laravel guard that uses the session driver.

The target guard is resolved in this order:

  1. The explicit guard: argument passed to Mirror::impersonate().
  2. A guardName() method on the target model.
  3. A guard_name attribute or default property on the target model.
  4. The first session guard whose provider model matches the target model.

Pass the guard explicitly when multiple guards use the same provider model:

Mirror::impersonate($user, guard: 'web');

Warning

The first matching guard is used when resolution is ambiguous. Do not rely on configuration order when the choice affects application behavior.

8. Only session-backed guards are supported

Mirror 2.x rejects guards that do not use Laravel's session driver.

This affects applications that previously attempted impersonation through:

  • API guards
  • token guards
  • stateless guards
  • custom guards without session persistence

Use a session-backed web guard for both the original user and impersonated target.

9. Signed context replaces redirect metadata

Mirror 2.x can store arbitrary signed context with an impersonation:

Mirror::impersonate(
    target: $user,
    context: [
        'reason' => 'Support request',
        'ticket_id' => 123,
        'source' => 'admin-panel',
    ],
);

Read it while impersonation is active:

$context = Mirror::context();

The same context is returned by leave():

$context = Mirror::leave();

Use only session-serializable values. Do not store model instances, closures, resources, or sensitive data that does not belong in the session.

10. Exceptions were redesigned

The 1.x ImpersonationException base exception and TamperedSessionException were removed.

All 2.x package exceptions extend:

Mirror\Exceptions\MirrorException

For most application code, catch an interface representing the operation phase:

use Mirror\Exceptions\CannotStartImpersonation;
use Mirror\Facades\Mirror;

try {
    Mirror::impersonate($user);
} catch (CannotStartImpersonation $exception) {
    report($exception);

    return back()->withErrors([
        'impersonation' => 'Impersonation could not be started.',
    ]);
}
use Mirror\Exceptions\CannotLeaveImpersonation;

try {
    $context = Mirror::leave();
} catch (CannotLeaveImpersonation $exception) {
    report($exception);
}

Phase interfaces

Interface Operations
CannotStartImpersonation Failures raised by impersonate()
CannotLeaveImpersonation Failures raised by leave()
CannotReadImpersonationState Failures raised while reading active state

Common concrete exceptions

  • CanNotImpersonate
  • CanNotBeImpersonated
  • CannotInferTargetGuard
  • GuardDoesNotUseSessionDriver
  • ImpersonationAlreadyActive
  • ImpersonationNotActive
  • InvalidImpersonationSignature
  • MissingAuthenticatedSessionGuard
  • MissingImpersonationSignature

Catch concrete exceptions only when the application needs a distinct response for that exact failure.

11. Nested impersonation is explicitly rejected

Starting a second impersonation while one is already active throws:

Mirror\Exceptions\ImpersonationAlreadyActive

Check state before presenting UI actions, but still handle the exception because state can change between the check and the operation:

if (! Mirror::active()) {
    Mirror::impersonate($user);
}

12. Events changed

The event class names remain:

  • Mirror\Events\ImpersonationStarted
  • Mirror\Events\ImpersonationStopped

Their public payload changed.

1.x event properties

$event->impersonator;
$event->impersonated;
$event->guardName;

2.x event properties

$event->impersonator;
$event->impersonated;
$event->context;

Update listeners that access $guardName:

use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Log;
use Mirror\Events\ImpersonationStarted;

Event::listen(ImpersonationStarted::class, function (ImpersonationStarted $event): void {
    Log::info('User impersonation started', [
        'impersonator_id' => $event->impersonator->getAuthIdentifier(),
        'impersonated_id' => $event->impersonated->getAuthIdentifier(),
        'context' => $event->context,
    ]);
});

Do not assume 1.x deferred-dispatch semantics in code that depends on exactly when listeners execute. Test any listener with transactional or response-timing requirements.

13. Blade directives

The primary directives remain available:

@impersonating
    <div>You are impersonating {{ auth()->user()->name }}.</div>
@endimpersonating
@canImpersonate
    <a href="{{ route('admin.users.index') }}">Manage users</a>
@endcanImpersonate
@canBeImpersonated($user)
    <button type="submit">Impersonate</button>
@endcanBeImpersonated

Mirror 2.x also provides:

@notImpersonating
    <span>Normal session</span>
@endnotImpersonating

Capability directives now rely on the Mirror\Contracts\Impersonatable contract. Models that previously depended on dynamically detected methods without implementing the trait or contract must be updated.

Guard-specific impersonation checks remain supported:

@impersonating('web')
    <span>Impersonating through the web guard</span>
@endimpersonating

14. Configuration changed

Removed options

The following 1.x options no longer exist:

'enabled' => env('MIRROR_ENABLED', true),
'guard' => null,
'default_redirect_url' => '/',

Replace them as follows:

Removed option Migration
enabled Use application configuration, feature flags, authorization, or route registration conditions.
guard Pass guard: to Mirror::impersonate() or define guard information on the target model.
default_redirect_url Redirect explicitly in application controllers or middleware.

TTL default changed

The 1.x default was:

'ttl' => null,

The 2.x default is:

'ttl' => 1800,

This means expiration checks are enabled by default with a 30-minute TTL.

Set it to null to disable expiration:

'ttl' => null,

Session namespace added

Mirror 2.x adds:

'session' => [
    'key' => env('MIRROR_SESSION_KEY', 'mirror.impersonation'),
],

The complete default configuration is:

return [
    'ttl' => 1800,

    'session' => [
        'key' => env('MIRROR_SESSION_KEY', 'mirror.impersonation'),
    ],
];

Note

Existing 1.x impersonation sessions should not be expected to survive deployment of 2.x because the stored payload format and session keys changed.

15. Suggested controller migration

1.x

use App\Models\User;
use Mirror\Facades\Mirror;

class ImpersonationController
{
    public function store(User $user)
    {
        $redirectUrl = Mirror::start(
            user: $user,
            leaveRedirectUrl: route('admin.users.index'),
            startRedirectUrl: route('dashboard'),
        );

        return redirect($redirectUrl);
    }

    public function destroy()
    {
        Mirror::stop();

        return redirect(Mirror::getLeaveRedirectUrl());
    }
}

2.x

use App\Models\User;
use Illuminate\Http\RedirectResponse;
use Mirror\Facades\Mirror;

class ImpersonationController
{
    public function store(User $user): RedirectResponse
    {
        Mirror::impersonate(
            target: $user,
            guard: 'web',
            context: [
                'started_from' => request()->fullUrl(),
            ],
        );

        return redirect()->route('dashboard');
    }

    public function destroy(): RedirectResponse
    {
        $context = Mirror::leave();

        return redirect()
            ->route('admin.users.index')
            ->with('impersonation_context', $context);
    }
}

16. Deployment considerations

Before deploying 2.x:

  1. End or invalidate active 1.x impersonation sessions.
  2. Publish and review the new configuration.
  3. Confirm that every relevant model implements Impersonatable.
  4. Confirm all relevant guards use the session driver.
  5. Replace removed route middleware.
  6. Update event listeners and exception handlers.
  7. Exercise start, active-state, expiration, leave, tampered-session, and multi-guard flows in tests.

A safe deployment may also clear existing Mirror session state by invalidating application sessions, depending on your security and availability requirements.

What's Changed

Full Changelog: v1.5.0...v2.0