v2.0
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 --forceWarning
Review your existing config/mirror.php before overwriting it. Several 1.x options no longer exist.
Upgrade checklist
- Replace the
Mirror\Concerns\Impersonatabletrait with theMirror\Contracts\Impersonatablecontract. - Implement both
canImpersonate()andcanBeImpersonated()explicitly. - Replace
Mirror::start()and its aliases withMirror::impersonate(). - Replace
Mirror::stop()orMirror::forceStop()withMirror::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
$contextinstead of$guardName. - Update
config/mirror.php. - Confirm every impersonator and target guard uses the Laravel
sessiondriver. - 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:
- The explicit
guard:argument passed toMirror::impersonate(). - A
guardName()method on the target model. - A
guard_nameattribute or default property on the target model. - 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\MirrorExceptionFor 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
CanNotImpersonateCanNotBeImpersonatedCannotInferTargetGuardGuardDoesNotUseSessionDriverImpersonationAlreadyActiveImpersonationNotActiveInvalidImpersonationSignatureMissingAuthenticatedSessionGuardMissingImpersonationSignature
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\ImpersonationAlreadyActiveCheck 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\ImpersonationStartedMirror\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>
@endcanBeImpersonatedMirror 2.x also provides:
@notImpersonating
<span>Normal session</span>
@endnotImpersonatingCapability 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>
@endimpersonating14. 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:
- End or invalidate active 1.x impersonation sessions.
- Publish and review the new configuration.
- Confirm that every relevant model implements
Impersonatable. - Confirm all relevant guards use the
sessiondriver. - Replace removed route middleware.
- Update event listeners and exception handlers.
- 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
- v2 rewrite by @franbarbalopez in #12
Full Changelog: v1.5.0...v2.0