cdoebler / php-generic-user-switcher
A framework-agnostic PHP package for user switching with a frontend component.
Installs: 25
Dependents: 1
Suggesters: 0
Security: 0
Stars: 0
Watchers: 0
Forks: 0
Open Issues: 0
pkg:composer/cdoebler/php-generic-user-switcher
Requires
- php: ^8.2
Requires (Dev)
- mockery/mockery: ^1.6
- pestphp/pest: ^3.0
- phpstan/phpstan: ^2.0
- rector/rector: ^2.2
This package is not auto-updated.
Last update: 2025-12-29 12:48:30 UTC
README
A framework-agnostic PHP package for user switching with an integrated frontend component. Perfect for development and debugging, allowing seamless impersonation of different users without logging in and out.
Features
- Framework-Agnostic: Works with any PHP application or framework
- Clean Interface-Based Design: Easy to extend and customize
- Session-Based Impersonation: Non-destructive user switching with preserved original user identity
- Visual UI Component: Beautiful, searchable dropdown widget for quick user switching
- Type-Safe: Built with PHP 8.2+ strict types and PHPStan level 10 validation
- Zero Dependencies: No external production dependencies
- Fully Tested: Comprehensive test coverage with Pest
Requirements
- PHP 8.2 or higher
- Session support (for default
SessionImpersonator)
Installation
Install via Composer:
composer require cdoebler/php-generic-user-switcher
Quick Start
Basic Usage
<?php use Cdoebler\GenericUserSwitcher\Generic\GenericUser; use Cdoebler\GenericUserSwitcher\Generic\InMemoryUserProvider; use Cdoebler\GenericUserSwitcher\Generic\SessionImpersonator; use Cdoebler\GenericUserSwitcher\Renderer\UserSwitcherRenderer; // 1. Create your users $users = [ new GenericUser(1, 'John Admin'), new GenericUser(2, 'Jane Developer'), new GenericUser(3, 'Bob Guest'), ]; // 2. Set up the user provider $userProvider = new InMemoryUserProvider($users); // 3. Set up the impersonator $impersonator = new SessionImpersonator(); // 4. Render the switcher in your layout/template $renderer = new UserSwitcherRenderer($userProvider, $impersonator); echo $renderer->render();
Handling User Switching
In your application's bootstrap or middleware, handle the switch request:
<?php // Handle user switching requests if (isset($_GET['_switch_user'])) { if ($_GET['_switch_user'] === '_stop') { $impersonator->stopImpersonating(); } else { $impersonator->impersonate($_GET['_switch_user']); } // Redirect to remove the parameter from URL header('Location: ' . strtok($_SERVER['REQUEST_URI'], '?')); exit; } // Get the current user (impersonated or real) if ($impersonator->isImpersonating()) { $currentUserId = $_SESSION['generic_user_switcher_impersonator']; $originalUserId = $impersonator->getOriginalUserId(); // Load the impersonated user $currentUser = $userProvider->findUserById($currentUserId); } else { // Load the actual logged-in user $currentUser = getCurrentUser(); // Your app's logic }
Usage Examples
Example 1: Simple In-Memory Users
<?php $users = [ new GenericUser(1, 'Admin User'), new GenericUser(2, 'Regular User'), new GenericUser(3, 'Guest User'), ]; $provider = new InMemoryUserProvider($users); $impersonator = new SessionImpersonator(); $renderer = new UserSwitcherRenderer($provider, $impersonator); // Render with default options (bottom-right position) echo $renderer->render();
Example 2: Custom Position and Styling
<?php echo $renderer->render([ 'position' => 'top-left', // Options: top-left, top-right, bottom-left, bottom-right 'z_index' => 10000, // CSS z-index for the widget 'param_name' => '_su', // Custom URL parameter name ]);
Example 3: Custom Session Key
<?php // Use a custom session key for storing impersonation state $impersonator = new SessionImpersonator('my_custom_session_key');
Example 4: Database User Provider
Create a custom provider that loads users from your database:
<?php namespace App\UserSwitcher; use Cdoebler\GenericUserSwitcher\Interfaces\UserInterface; use Cdoebler\GenericUserSwitcher\Interfaces\UserProviderInterface; class DatabaseUserProvider implements UserProviderInterface { public function __construct( private \PDO $pdo ) {} public function getUsers(): array { $stmt = $this->pdo->query('SELECT id, name FROM users ORDER BY name'); $users = []; while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { $users[] = new \Cdoebler\GenericUserSwitcher\Generic\GenericUser( $row['id'], $row['name'] ); } return $users; } public function findUserById(string|int $identifier): ?UserInterface { $stmt = $this->pdo->prepare('SELECT id, name FROM users WHERE id = ?'); $stmt->execute([$identifier]); $row = $stmt->fetch(\PDO::FETCH_ASSOC); if (!$row) { return null; } return new \Cdoebler\GenericUserSwitcher\Generic\GenericUser( $row['id'], $row['name'] ); } } // Usage $pdo = new PDO('mysql:host=localhost;dbname=myapp', 'user', 'pass'); $provider = new DatabaseUserProvider($pdo); $renderer = new UserSwitcherRenderer($provider, $impersonator);
Example 5: Custom User Implementation
Implement the UserInterface for your existing user models:
<?php namespace App\Models; use Cdoebler\GenericUserSwitcher\Interfaces\UserInterface; class User implements UserInterface { public function __construct( private int $id, private string $email, private string $firstName, private string $lastName, ) {} public function getIdentifier(): string|int { return $this->id; } public function getDisplayName(): string { return "{$this->firstName} {$this->lastName} ({$this->email})"; } // Your other user methods... }
Example 6: Laravel Integration
<?php // app/UserSwitcher/LaravelUserProvider.php namespace App\UserSwitcher; use App\Models\User; use Cdoebler\GenericUserSwitcher\Interfaces\UserInterface; use Cdoebler\GenericUserSwitcher\Interfaces\UserProviderInterface; use Cdoebler\GenericUserSwitcher\Generic\GenericUser; class LaravelUserProvider implements UserProviderInterface { public function getUsers(): array { return User::orderBy('name') ->get() ->map(fn($user) => new GenericUser($user->id, $user->name)) ->all(); } public function findUserById(string|int $identifier): ?UserInterface { $user = User::find($identifier); return $user ? new GenericUser($user->id, $user->name) : null; } } // app/Http/Middleware/HandleUserSwitcher.php namespace App\Http\Middleware; use Closure; use Illuminate\Http\Request; class HandleUserSwitcher { public function handle(Request $request, Closure $next) { if ($request->has('_switch_user') && app()->environment('local')) { $impersonator = app(\Cdoebler\GenericUserSwitcher\Generic\SessionImpersonator::class); if ($request->get('_switch_user') === '_stop') { $impersonator->stopImpersonating(); } else { $impersonator->impersonate($request->get('_switch_user')); } return redirect($request->url()); } return $next($request); } } // resources/views/layouts/app.blade.php @if(app()->environment('local')) {!! app(\Cdoebler\GenericUserSwitcher\Renderer\UserSwitcherRenderer::class)->render() !!} @endif
Example 7: Symfony Integration
<?php // src/UserSwitcher/DoctrineUserProvider.php namespace App\UserSwitcher; use App\Entity\User; use Cdoebler\GenericUserSwitcher\Generic\GenericUser; use Cdoebler\GenericUserSwitcher\Interfaces\UserInterface; use Cdoebler\GenericUserSwitcher\Interfaces\UserProviderInterface; use Doctrine\ORM\EntityManagerInterface; class DoctrineUserProvider implements UserProviderInterface { public function __construct( private EntityManagerInterface $entityManager ) {} public function getUsers(): array { $users = $this->entityManager ->getRepository(User::class) ->findBy([], ['email' => 'ASC']); return array_map( fn(User $user) => new GenericUser($user->getId(), $user->getEmail()), $users ); } public function findUserById(string|int $identifier): ?UserInterface { $user = $this->entityManager->find(User::class, $identifier); return $user ? new GenericUser($user->getId(), $user->getEmail()) : null; } } // config/services.yaml services: App\UserSwitcher\DoctrineUserProvider: ~ Cdoebler\GenericUserSwitcher\Generic\SessionImpersonator: ~ Cdoebler\GenericUserSwitcher\Renderer\UserSwitcherRenderer: arguments: $userProvider: '@App\UserSwitcher\DoctrineUserProvider' $impersonator: '@Cdoebler\GenericUserSwitcher\Generic\SessionImpersonator' // templates/base.html.twig {% if app.environment == 'dev' %} {{ render_user_switcher()|raw }} {% endif %}
Configuration Options
Renderer Options
The UserSwitcherRenderer::render() method accepts an array of options:
| Option | Type | Default | Description |
|---|---|---|---|
position |
string |
'bottom-right' |
Widget position: top-left, top-right, bottom-left, bottom-right |
z_index |
int |
9999 |
CSS z-index value for the floating widget |
param_name |
string |
'_switch_user' |
URL query parameter name used for switching users |
SessionImpersonator Options
<?php // Default session key $impersonator = new SessionImpersonator(); // Custom session key $impersonator = new SessionImpersonator('my_app_impersonator');
Interface Reference
UserInterface
interface UserInterface { public function getIdentifier(): string|int; public function getDisplayName(): string; }
UserProviderInterface
interface UserProviderInterface { /** @return array<UserInterface> */ public function getUsers(): array; public function findUserById(string|int $identifier): ?UserInterface; }
ImpersonatorInterface
interface ImpersonatorInterface { public function impersonate(string|int $identifier): void; public function stopImpersonating(): void; public function isImpersonating(): bool; public function getOriginalUserId(): string|int|null; }
Security Considerations
This package is designed for development and debugging purposes only.
Important security notes:
- Never enable in production - User switching should only be available in development/staging environments
- Add authorization checks - Ensure only authorized users (admins, developers) can switch users
- Environment gating - Use environment checks to disable in production:
<?php // Only show switcher in development if (getenv('APP_ENV') === 'development') { echo $renderer->render(); }
- Audit logging - Consider logging all impersonation events for security auditing
Example: Implementing Audit Logging
The package supports optional audit logging to track all impersonation events:
<?php use Cdoebler\GenericUserSwitcher\Interfaces\AuditLoggerInterface; class DatabaseAuditLogger implements AuditLoggerInterface { public function __construct( private \PDO $pdo, private int $currentUserId ) {} public function logImpersonationStarted(string|int $targetUserId): void { $stmt = $this->pdo->prepare( 'INSERT INTO audit_log (user_id, action, target_user_id, created_at) VALUES (?, ?, ?, NOW())' ); $stmt->execute([$this->currentUserId, 'impersonation_started', $targetUserId]); } public function logImpersonationStopped(): void { $stmt = $this->pdo->prepare( 'INSERT INTO audit_log (user_id, action, created_at) VALUES (?, ?, NOW())' ); $stmt->execute([$this->currentUserId, 'impersonation_stopped']); } } // Usage $auditLogger = new DatabaseAuditLogger($pdo, $currentUser->getId()); $impersonator = new SessionImpersonator('generic_user_switcher_impersonator', $auditLogger);
Development
Running Tests
# Run all tests composer test # Run only Pest tests composer pest # Run PHPStan analysis composer phpstan # Run Rector (dry-run) composer rector-dry # Apply Rector fixes composer rector
Code Quality
This package maintains high code quality standards:
- PHPStan Level 10: Maximum static analysis strictness
- Pest Testing: Modern PHP testing with comprehensive coverage
- Rector: Automated code modernization and consistency
- Architecture Tests: Enforces coding standards and best practices
Architecture
┌─────────────────────────────────┐
│ UserSwitcherRenderer │
│ (Frontend Component) │
└────────┬────────────┬───────────┘
│ │
▼ ▼
┌──────────────┐ ┌──────────────────┐
│UserProvider │ │ Impersonator │
│Interface │ │ Interface │
└──────┬───────┘ └────────┬─────────┘
│ │
▼ ▼
┌──────────────┐ ┌──────────────────┐
│InMemory │ │ Session │
│UserProvider │ │ Impersonator │
└──────────────┘ └──────────────────┘
│
▼
┌──────────────┐
│UserInterface │
└──────┬───────┘
│
▼
┌──────────────┐
│GenericUser │
└──────────────┘
Contributing
Contributions are welcome! Please ensure:
- All tests pass (
composer test) - Code follows PSR-12 standards
- PHPStan level 10 passes
- New features include tests
License
MIT License. See LICENSE.md file for details.
Author
Christian Doebler
- Email: mail@christian-doebler.net
- GitHub: @cdoebler
Changelog
1.0.0 (Current)
- Initial release
- Framework-agnostic user switching
- Session-based impersonation
- Frontend UI component
- In-memory user provider
- Generic user implementation
- Full test coverage