mindtwo / laravel-platform-manager
A pacakage to resolve platforms.
Package info
github.com/mindtwo/laravel-platform-manager
pkg:composer/mindtwo/laravel-platform-manager
Requires
- php: ^8.3||^8.4||^8.5
- chiiya/laravel-utilities: ^5.7
- laravel/framework: ^12.0
Requires (Dev)
- barryvdh/laravel-ide-helper: ^3.6
- larastan/larastan: ^3.0
- laravel/pint: ^1.4
- orchestra/testbench: ^10.0
- pestphp/pest: ^4.0
- pestphp/pest-plugin-arch: ^4.0
- pestphp/pest-plugin-laravel: ^4.0
- phpstan/extension-installer: ^1.4
- phpstan/phpstan-deprecation-rules: ^2.0
- dev-master
- 4.0.0
- 2.7.1
- 2.7.0
- 2.6.4
- 2.6.3
- 2.6.2
- 2.6.1
- 2.6
- 2.5
- 2.4
- 2.3
- 2.2
- 2.1.14.x-dev
- 2.1.14
- 2.1.13.x-dev
- 2.1.13
- 2.1.12
- 2.1.11.x-dev
- 2.1.11
- 2.1.10
- 2.1.9
- 2.1.8
- 2.1.7
- 2.1.6
- 2.1.5
- 2.1.4
- 2.1.3
- 2.1.2
- 2.1.1
- 2.1
- 2.0.1
- 2.0
- 1.12.1
- 1.12
- 1.11
- 1.10
- 1.9.13
- 1.9.12
- 1.9.11
- 1.9.10
- 1.9.6
- 1.9.5
- 1.9.4
- 1.9.3
- 1.9.2
- 1.9.1
- 1.9
- 1.8.1
- 1.8
- 1.7.5
- 1.7.4
- 1.7.3
- 1.7.2
- 1.7.1
- 1.7.0
- 1.6.2
- 1.6.1
- 1.6
- 1.5
- 1.4
- 1.3
- 1.2
- 1.1
- 1.0
- 0.1
- dev-add-ai-guidelines
- dev-feature/extend-platform-again
- dev-feature/remove-legacy-webhook-stuff
- dev-hotfix/dispatch-configuration-model
- dev-origin/2.3
- dev-feat/webhooks-v2
- dev-feature/update-to-laravel-10
This package is auto-updated.
Last update: 2026-03-18 13:39:25 UTC
README
Resolve a "platform" (tenant, site, or API client) on every request — by hostname, token, context string, or session — and make it available everywhere via platform().
Installation
composer require mindtwo/laravel-platform-manager
Publish config
php artisan vendor:publish --provider="mindtwo\LaravelPlatformManager\LaravelPlatformManagerProvider" --tag=config
This publishes config/platform.php.
Publish and run migrations
php artisan vendor:publish --provider="mindtwo\LaravelPlatformManager\LaravelPlatformManagerProvider" --tag=migrations
php artisan migrate
Configuration
// config/platform.php return [ // Eloquent model used as the platform. Swap this for your own model that extends Platform. 'model' => \mindtwo\LaravelPlatformManager\Models\Platform::class, // HTTP headers used for M2M token auth. 'header_names' => [ 'token' => 'X-Platform-Token', // Legacy header accepted during a grace period. Set to null to disable. 'token_legacy' => 'X-Context-Platform-Public-Auth-Token', ], // Session key used by the session resolver. 'session_key' => 'platform_id', ];
Middleware
Register the middleware alias in your application's middleware stack or use it inline on routes:
// routes/api.php Route::middleware('resolve-platform:token')->group(function () { // platform() is available here });
The resolve-platform alias is registered automatically by the service provider.
Resolver strategies
Pass one or more strategies separated by |. The first one that returns a match wins.
| Strategy | Resolves by |
|---|---|
host |
Host header matched against hostname / additional_hostnames (supports * wildcards) |
token |
X-Platform-Token header matched against an active, non-expired auth_tokens record |
context |
X-Platform-Context header matched against the context column |
session |
Platform PK stored in the session via platform()->saveToSession() |
// Try token first, fall back to hostname Route::middleware('resolve-platform:token|host')->group(function () { ... });
If no strategy resolves a platform the middleware aborts with a 404.
The platform() helper
The global platform() function returns the singleton Platform context object.
// Check whether a platform has been resolved platform()->isResolved(); // bool // Get the underlying Eloquent model platform()->get(); // ?PlatformModel // Read any model attribute directly platform()->hostname; platform()->uuid; // Read platform settings (dot notation) platform()->setting('mail.from'); platform()->setting('billing.plan', 'free'); // Which resolver matched platform()->resolver(); // 'token' | 'host' | 'context' | 'session' | ...
Scopes
Scopes control what operations a resolved platform is allowed to perform. There are two layers:
- Platform baseline scopes — stored on the
platformsrow itself, always active regardless of how the platform was resolved. - Token scopes — carried by an
auth_tokensrecord, merged on top of the baseline when the platform is resolved via thetokenstrategy.
The effective scope set is platform.scopes ∪ token.scopes.
Platform baseline scopes
$platform->update(['scopes' => ['read']]);
These scopes apply for every resolver (host, session, context, token).
M2M token auth & scopes
Auth tokens are M2M (machine-to-machine) credentials stored in the auth_tokens table. Token scopes widen the platform's baseline — they cannot narrow it.
Creating a token
$platform->authTokens()->create([ 'scopes' => ['read', 'write'], ]);
Checking scopes in application code
platform()->can() returns true when the scope is present in the effective set (platform baseline + token scopes).
// In a controller, middleware, policy, etc. if (! platform()->can('write')) { abort(403); }
Token model helpers
$token->hasScope('admin'); // bool $token->scopes; // array<string> $token->isExpired(); // bool AuthToken::withScope('read')->get(); // query scope
Expiry
Set expired_at to limit a token's lifetime. Expired tokens are ignored by the middleware resolver automatically.
$platform->authTokens()->create([ 'scopes' => ['read'], 'expired_at' => now()->addDays(30), ]);
Session-based resolution
// Store the current platform in the session (e.g. after an admin selects a platform) platform()->saveToSession($platformModel); // Or if it's already set: platform()->set($model, 'admin'); platform()->saveToSession(); // Clear on logout / platform switch platform()->clearFromSession();
Temporary platform context (use())
Switch platform for the duration of a callback, then restore the previous context automatically — even if the callback throws.
platform()->use($otherPlatform, function () { // platform() resolves $otherPlatform here Mail::send(...); }); // platform() is restored here
Queue jobs
Use the HasPlatformContext trait to capture and restore platform context across queue boundaries.
use mindtwo\LaravelPlatformManager\Jobs\Concerns\HasPlatformContext; class ProcessOrder implements ShouldQueue { use HasPlatformContext; public function __construct(private Order $order) { $this->capturePlatformContext(); // call at end of constructor } public function handle(): void { $this->restorePlatformContext(); // call at start of handle // platform() is now resolved } }
Extending the Platform model
Publish the config and point platform.model at your own model:
// app/Models/Platform.php use mindtwo\LaravelPlatformManager\Models\Platform as BasePlatform; class Platform extends BasePlatform { // add columns, relationships, scopes ... }
// config/platform.php 'model' => \App\Models\Platform::class,
BelongsToPlatform trait
Add the trait to any Eloquent model that belongs to a platform. It auto-fills platform_id on create and provides two query scopes.
use mindtwo\LaravelPlatformManager\Traits\BelongsToPlatform; class Article extends Model { use BelongsToPlatform; } // Scopes Article::forCurrentPlatform()->get(); Article::forPlatform($platform)->get(); Article::forPlatform(42)->get();
Scope middleware
The platform-scope middleware aborts with 403 if the resolved platform does not hold the required scope(s). Apply it after resolve-platform.
Route::middleware(['resolve-platform:token', 'platform-scope:write'])->group(function () { // platform must have the 'write' scope }); // Multiple scopes — all must be present Route::middleware(['resolve-platform:token', 'platform-scope:read,write'])->group(function () { // ... });
BelongsToManyPlatforms trait
For models that belong to multiple platforms via a pivot table. Provides the same scopes as BelongsToPlatform but uses whereHas under the hood.
use mindtwo\LaravelPlatformManager\Traits\BelongsToManyPlatforms; class Article extends Model { use BelongsToManyPlatforms; } // Scopes Article::forCurrentPlatform()->get(); Article::forPlatform($platform)->get(); Article::forPlatform(42)->get(); // Relationship $article->platforms; // Collection of Platform models
The pivot table is derived automatically as platform_{models} (e.g. platform_articles). Override getPlatformPivotTable() on the model to use a different name:
public function getPlatformPivotTable(): string { return 'article_platform'; }
Typed platform settings
Platform settings are stored as JSON in the settings column and hydrated into a PlatformSettings DTO. Declare known properties as typed public fields and list any that should be encrypted at rest in $encrypted.
Extending PlatformSettings
// app/Settings/PlatformSettings.php use mindtwo\LaravelPlatformManager\Settings\PlatformSettings as BaseSettings; class PlatformSettings extends BaseSettings { protected array $encrypted = ['apiSecret', 'smtpPassword']; public ?string $appName = null; public ?string $apiSecret = null; // encrypted at rest public ?string $smtpPassword = null; // encrypted at rest public ?string $billingPlan = null; }
Point the config at your class:
// config/platform.php 'settings' => \App\Settings\PlatformSettings::class,
Reading settings
// Via the helper (dot notation, works for any depth) platform()->setting('appName'); platform()->setting('mail.host', 'localhost'); // nested via overflow // Via the model directly $platform->settings->appName; $platform->setting('appName');
Writing settings
// Assign properties directly $platform->settings->appName = 'My App'; $platform->settings->apiSecret = 's3cr3t'; // stored encrypted $platform->save(); // Or replace the whole DTO $platform->update(['settings' => ['appName' => 'My App', 'apiSecret' => 's3cr3t']]);
Unknown keys (no matching declared property) are stored transparently in an overflow bag so existing data and config overrides continue to work without any changes.
Config overrides
A platform can override arbitrary Laravel config values by storing them under settings.config:
$platform->update([ 'settings' => [ 'config' => [ 'mail.default' => 'ses', 'app.name' => 'My Platform', ], ], ]);
These overrides are applied automatically whenever the platform is resolved.
PlatformRepository
All platform lookups go through PlatformRepository, which extends chiiya/laravel-utilities's AbstractRepository. The middleware resolves it automatically, but you can also inject it directly.
use mindtwo\LaravelPlatformManager\Repositories\PlatformRepository; class PlatformController extends Controller { public function __construct(protected PlatformRepository $platforms) {} }
Request-aware resolvers
These map directly to the middleware strategies and read from the incoming request:
$repository->resolveByToken($request); // ?array{PlatformModel, array<string>} $repository->resolveByHostname($request); // ?PlatformModel $repository->resolveByContext($request); // ?PlatformModel $repository->resolveBySession($request); // ?PlatformModel
resolveByToken returns a tuple of [PlatformModel, effectiveScopes] where scopes are the platform baseline merged with the token's scopes.
Value-based finders
$repository->findByHostname('example.com'); // ?PlatformModel $repository->findByContext('my-context'); // ?PlatformModel $repository->findByUuid('uuid-string'); // ?PlatformModel $repository->findActiveById(1); // ?PlatformModel // Returns [PlatformModel, effectiveScopes]|null $repository->findByTokenWithScopes($rawToken);
Collection queries
$repository->allActive(); // Collection<PlatformModel> $repository->index(['is_active' => true]); // Collection<PlatformModel> $repository->index(['hostname' => 'app.io']); // Collection<PlatformModel> $repository->count(['is_active' => true]); // int $repository->search('app', ['is_active' => true]); // LengthAwarePaginator
Supported applyFilters parameters: is_active, hostname, context.
Testing
PlatformFake
Set a fake platform on the singleton without hitting the database. Useful in any test that exercises code which calls platform().
use mindtwo\LaravelPlatformManager\Testing\PlatformFake; PlatformFake::make(['hostname' => 'test.com']); // With resolver and scopes PlatformFake::make(['hostname' => 'test.com'], resolver: 'token', scopes: ['read', 'write']); // Reset back to unresolved PlatformFake::reset();
InteractsWithPlatform trait
Add to your test case for a cleaner API and automatic teardown helpers:
use mindtwo\LaravelPlatformManager\Testing\InteractsWithPlatform; class MyTest extends TestCase { use InteractsWithPlatform; protected function tearDown(): void { $this->clearPlatform(); parent::tearDown(); } public function test_something(): void { $this->setPlatform(['hostname' => 'test.com'], scopes: ['read']); $this->assertPlatformResolved(); $this->assertPlatformCan('read'); $this->assertPlatformCannot('write'); $this->assertPlatformResolver('fake'); } }
| Method | Description |
|---|---|
setPlatform(array $attributes, string $resolver, array $scopes) |
Resolve a fake platform |
clearPlatform() |
Reset the singleton to unresolved |
assertPlatformResolved() |
Assert a platform is resolved |
assertPlatformNotResolved() |
Assert no platform is resolved |
assertPlatformCan(string $scope) |
Assert the platform has a scope |
assertPlatformCannot(string $scope) |
Assert the platform lacks a scope |
assertPlatformResolver(string $resolver) |
Assert the resolver name |
Upgrade Guide
Upgrading from v2 to v4
v4 is a full rewrite. Every section below is a breaking change — work through them in order.
Step 1 — Service provider
The provider moved out of the Providers sub-namespace.
// Before (config/app.php or auto-discovery override) mindtwo\LaravelPlatformManager\Providers\LaravelPlatformManagerProvider::class // After mindtwo\LaravelPlatformManager\LaravelPlatformManagerProvider::class
Step 2 — Config file rename and restructure
The config file was renamed from platform-resolver.php to platform.php, and the config key changed from platform-resolver to platform.
# Republish the config php artisan vendor:publish --provider="mindtwo\LaravelPlatformManager\LaravelPlatformManagerProvider" --tag=config
Key mapping:
// Before (config/platform-resolver.php) 'model' => Platform::class, 'headerNames' => [ AuthTokenTypeEnum::Public() => 'X-Context-Platform-Public-Auth-Token', AuthTokenTypeEnum::Secret() => 'X-Context-Platform-Secret-Auth-Token', ], 'webhooks' => [ ... ], // After (config/platform.php) 'model' => Platform::class, 'header_names' => [ 'token' => 'X-Platform-Token', ], 'session_key' => 'platform_id',
Update any config('platform-resolver.*') calls in your own code to config('platform.*').
Step 3 — Platforms table migration
Several columns were removed and three were added. Create a migration in your application:
Schema::table('platforms', function (Blueprint $table) { // Remove v2-only columns (skip any you wish to keep in your own schema) $table->dropColumn([ 'owner_id', 'is_main', 'is_headless', 'name', 'default_locale', 'available_locales', ]); // Widen hostname to 100 chars $table->string('hostname', 100)->nullable()->change(); // Add new columns $table->string('context')->nullable()->unique()->after('additional_hostnames'); $table->json('scopes')->nullable()->after('context'); $table->json('settings')->nullable()->after('scopes'); });
Step 4 — Auth tokens table migration
The type column is replaced by scopes, expired_at is added, and several v2 columns are dropped:
Schema::table('auth_tokens', function (Blueprint $table) { // Drop v2 columns $table->dropForeign(['user_id']); $table->dropUnique(['platform_id', 'token']); // composite unique $table->dropColumn(['user_id', 'description']); $table->dropSoftDeletes(); $table->dropColumn('type'); // Add v4 columns $table->json('scopes')->default('[]')->after('platform_id'); $table->datetime('expired_at')->nullable()->after('token'); });
Migrate existing token types to scopes before dropping type if you need to preserve access levels:
// Run before dropping 'type' DB::table('auth_tokens')->where('type', 1)->update(['scopes' => '["read","write"]']); // Secret → full access DB::table('auth_tokens')->where('type', 2)->update(['scopes' => '["read"]']); // Public → read only
Step 5 — Replace PlatformResolver with platform()
The PlatformResolver service is gone. Replace all usages with the platform() helper or app(Platform::class).
// Before app(PlatformResolver::class)->getCurrentPlatform() resolve(PlatformResolver::class)->getCurrentPlatform() // After platform()->get()
// Before — auth check app(PlatformResolver::class)->checkAuth(AuthTokenTypeEnum::Secret()) // After — scope check platform()->can('write')
Step 6 — Replace middleware
The old middleware classes are removed. Replace them with the new resolve-platform middleware.
// Before \mindtwo\LaravelPlatformManager\Middleware\PlatformSession::class // After 'resolve-platform:session'
// Before — token-based routes \mindtwo\LaravelPlatformManager\Middleware\ResolveBySecretToken::class \mindtwo\LaravelPlatformManager\Middleware\ResolveByPublicToken::class // After 'resolve-platform:token'
Multiple strategies can be chained:
Route::middleware('resolve-platform:token|host|session')->group(...);
Remove StatefulPlatformDomains — it is no longer part of this package.
Step 7 — Update API clients
Send the single X-Platform-Token header instead of the separate public/secret headers:
# Before
X-Context-Platform-Secret-Auth-Token: <token>
X-Context-Platform-Public-Auth-Token: <token>
# After
X-Platform-Token: <token>
Step 8 — Remove webhooks
The full webhook system (tables, jobs, routes, Nova resources) has been removed. If your application used webhooks:
- Drop the
webhooksandwebhook_requeststables - Remove any references to
PushToWebhook,WebhookController,WebhookConfiguration,WebhookRequest,EnsureWebhooksAreEnabled - Remove the
platform-resolver.webhooksconfig section (gone with step 2)
Step 9 — Nova resources
All built-in Nova resources have been removed. If you extended them, copy the field definitions into your own resource classes.
Upgrading to v4 (from v3)
v4 is a breaking release. The changes below are required.
1. Auth token type → scopes
The type column (Public/Secret) has been replaced with a scopes JSON array.
Migration — if you published the migration previously, update your create_auth_tokens_table migration (or create a new migration on existing tables):
// Before $table->smallInteger('type'); // After $table->json('scopes')->default('[]');
For existing tables, create a new migration:
Schema::table('auth_tokens', function (Blueprint $table) { $table->json('scopes')->default('[]')->after('platform_id'); $table->dropColumn('type'); });
Code — replace all AuthTokenTypeEnum references:
// Before $token->type = AuthTokenTypeEnum::Secret; $token->type = AuthTokenTypeEnum::Public; // After — just assign scopes $token->scopes = ['read', 'write'];
2. Header names config
// Before 'header_names' => [ 'public' => 'X-Context-Platform-Public-Auth-Token', 'secret' => 'X-Context-Platform-Secret-Auth-Token', ], // After 'header_names' => [ 'token' => 'X-Platform-Token', ],
Update any API clients to send X-Platform-Token (or whatever you configure) instead of the old public/secret headers.
3. Middleware resolver names
// Before Route::middleware('resolve-platform:public-token|secret-token|host')->group(...); // After Route::middleware('resolve-platform:token|host')->group(...);
4. Platform model scopes
// Before Platform::query()->byPublicAuthToken($token)->first(); Platform::query()->bySecretAuthToken($token)->first(); // After — single scope, expiry checked automatically Platform::query()->byToken($token)->first();
5. AuthTokenTypeEnum removed
Delete any imports or references to mindtwo\LaravelPlatformManager\Enums\AuthTokenTypeEnum. The enum no longer exists.
6. Nova resource removed
The built-in AuthToken Nova resource has been removed. If you extended it, update your subclass to work without the base class or reimplement the fields directly. The scopes field is a JSON array — a Tag or Text field works well.
7. New: platform()->can()
Scope authorization is now available for all resolvers, not just token:
if (platform()->can('write')) { // scope is in platform baseline or widened by the resolved token }
Platform baseline scopes (platforms.scopes) apply for every resolver. Token scopes are additive on top.
Changelog
Please see CHANGELOG for more information on what has changed recently.
Contributing
Please see CONTRIBUTING for details.
Security
If you discover any security related issues, please email info@mindtwo.de instead of using the issue tracker.
Credits
License
The MIT License (MIT). Please see License File for more information.