casbin/psr3-bridge
Integrates Casbin's PHP enforcement library with any PSR-3 compatible logger. This bridge adapts PSR-3 loggers to Casbin's logger interface, letting you route authorization decisions, policy loading, and role manager events into your existing logging stack.
casbin/psr3-bridge is a one-class adapter: it implements Casbin's Casbin\Log\LoggerInterface and forwards every logInfo / logError / logEnforce / logModel / logPolicy / logRole call to a Psr\Log\LoggerInterface you hand it. In a Laravel app that means "make Casbin's internal authorization chatter show up in Monolog," which is the only reason you'd install it.
Install alongside the core authorizer:
composer require casbin/casbin casbin/psr3-bridge
Before writing anything, open two files:
vendor/casbin/psr3-bridge/src/ — it's essentially one file. Read the constructor and the method mapping so you know exactly which PSR-3 level each Casbin method hits.vendor/casbin/casbin/src/Log/LoggerInterface.php — this is the contract the bridge satisfies. Note whether your pinned Casbin version's logEnforce takes the $explains array; the bridge was last published in early 2022 and the interface has moved since.The first use case is almost always debugging a failing policy check. Casbin is silent by default; once the bridge is wired in you can see the loaded model, the policies under consideration, the matcher being evaluated, and the request/result pair — which is exactly the information you're missing when Enforcer::enforce() returns false and you don't know why.
Minimal wiring, typically in a CasbinServiceProvider:
use Casbin\Bridge\Psr3\Logger as CasbinLogger;
use Casbin\Enforcer;
$this->app->singleton(Enforcer::class, function ($app) {
$psr3 = $app->make('log')->channel('casbin'); // Illuminate\Log\Logger is PSR-3
$logger = new CasbinLogger($psr3);
$logger->enableLog($app->environment(['local', 'staging']));
return new Enforcer(
config_path('casbin/model.conf'),
config_path('casbin/policy.csv'),
$logger,
);
});
Add a dedicated channel in config/logging.php so authorization noise doesn't pollute laravel.log:
'casbin' => [
'driver' => 'daily',
'path' => storage_path('logs/casbin.log'),
'level' => 'debug',
'days' => 7,
],
Singleton Enforcer with the logger baked in. Casbin's Enforcer accepts the logger as its third constructor argument. Bind the Enforcer as a singleton in the container and resolve it everywhere (app(Enforcer::class)), so the model/policy are loaded once and every authorization decision in the request lifecycle funnels through the same logger instance.
Environment-gated enablement. enableLog($enabled) is the switch. Keep it false in production — Casbin's info-level output is per-enforcement and will flood disk. Use $app->environment(['local', 'staging']) or a config('casbin.log') flag, and let the channel's level be a second safety net.
Route Casbin's logInfo to debug. The bridge maps logInfo to PSR-3 info by default, which is louder than you want. Either set the channel level to debug and accept that most Casbin lines are trace-only, or subclass the bridge (see Gotchas) and remap.
Testing with a spy or NullLogger. Because the logger is a constructor dependency, tests get a clean seam. For unit tests of policy logic, pass new NullLogger() to keep output silent. For integration tests asserting that a decision was logged, use Log::spy() and assert against the casbin channel, or bind a small in-memory PSR-3 logger that records calls.
$logger = new CasbinLogger(Log::channel('casbin'));
$enforcer = new Enforcer($model, $adapter, $logger);
$logger->enableLog(true);
$enforcer->enforce('alice', 'data1', 'read');
Log::channel('casbin')->shouldHaveReceived('debug'); // or assert on a spy
Pair with enforceEx() for explanations + logging. enforceEx() returns the matched policy as the second element of its tuple, which is far more useful than the boolean. Log the explanation yourself (via Log::withContext) and let the bridge cover the low-level model/policy dump. The two together give you "which rule matched" and "what the enforcer was thinking."
Middleware for request context. Casbin's logger methods don't receive HTTP context — no user ID, route, or request ID. Push those into Monolog's shared context with Log::withContext([...]) in middleware before the Enforcer runs, so every bridge line is automatically tagged.
app/Support/Casbin/Psr3Logger.php, implement Casbin\Log\LoggerInterface, and delete the dependency. You lose nothing. Confirm the namespace in vendor/casbin/psr3-bridge/src/ (Casbin\Bridge\Psr3\Logger in most releases) before importing it.logEnforce may have an $explains fourth parameter the bridge doesn't declare, and class ... implements LoggerInterface will fatal on the method signature. Pin both in composer.json (casbin/casbin matched to the bridge's era) or subclass and override with the modern signature.casbin log lines ever appear. Check $logger->isEnabled() and that the Enforcer actually received the instance.$request, $result, and $explains are stringified and lost as fields. If you need to query on them (e.g., in a log aggregator), extend the bridge and override logEnforce to emit a context array:
public function logEnforce($matcher, $request, $result, $explains = []): void
{
$this->logger->debug('casbin.enforce', compact('matcher', 'request', 'result', 'explains'));
}
logModel / logPolicy dump the entire policy set. That happens once per model load — if you're resolving the Enforcer as a singleton, it's once per request; if you're new Enforcer inside a loop, you'll dump every policy on every iteration. Another reason to bind it as a singleton in the container.Log::channel() on every call. $app->make('log')->channel('casbin') returns a fresh Illuminate\Log\Logger wrapper each time; construct the bridge once at binding time so the channel and handler stack are reused.logModel and logPolicy are the noisy ones; logEnforce is the valuable one).How can I help you explore Laravel packages today?