zoltanka/bypass-readonly
PHPUnit plugin that lets you bypass PHP readonly and final restrictions for testing. Useful when you need to mock, extend, or modify classes marked final/readonly without changing production code. Inspired by dg/bypass-finals.
Installation
composer require zoltanka/bypass-readonly
Add the service provider to config/app.php under providers:
Zoltanka\BypassReadonly\BypassReadonlyServiceProvider::class,
First Use Case
Temporarily bypass Laravel's readonly property on Eloquent models:
use Zoltanka\BypassReadonly\BypassReadonly;
$model = Model::first();
$bypass = new BypassReadonly($model);
$model->name = 'New Value'; // Now writable
$bypass->restore(); // Re-enable readonly
Where to Look First
BypassReadonly::bypass($model) for quick usage.bypass_readonly($model).tests/ directory for edge cases (e.g., nested relations).Mass Assignment Bypass
$models = Model::all();
$bypass = new BypassReadonly($models); // Bypasses for all models
Model::update([...]); // Now writable
$bypass->restore();
API Request Handling
public function update(Request $request, Model $model) {
bypass_readonly($model); // Global helper
$model->update($request->validated());
// readonly is auto-restored after method ends
}
Seeding Data
public function run() {
bypass_readonly(User::class); // Bypass for all instances of User
User::create([...]);
// readonly restored after seed completes
}
public function handle($request, Closure $next) {
bypass_readonly($request->user()->load('profile'));
return $next($request);
}
eloquent.saving to conditionally bypass:
Model::saving(function ($model) {
if (auth()->check()) {
bypass_readonly($model);
}
});
$this->partialMock(BypassReadonly::class, function ($mock) {
$mock->shouldReceive('restore')->never();
});
Memory Leaks
restore() can leave models in a writable state indefinitely.try-finally or Laravel's ensure():
bypass_readonly($model)->ensure(function () use ($model) {
$model->name = 'Updated';
}); // Auto-restores
Nested Relations
bypass_readonly($model)->with(['relation']);
Mass Assignment Risks
$fillable checks:
bypass_readonly($model);
$model->fill($request->only($model->fillable));
Observer/Event Conflicts
retrieved may trigger before restore().bypass_readonly()->restoreLater() to defer restoration.if (bypass_readonly()->isBypassed($model)) {
// Model is writable
}
trait LogsBypass {
protected static function booted() {
static::saved(function ($model) {
if (bypass_readonly()->isBypassed($model)) {
Log::warning("Readonly bypassed for {$model->getMorphClass()}");
}
});
}
}
Custom Conditions
Override the shouldBypass() logic in a custom service provider:
public function register() {
$this->app->bind(BypassReadonly::class, function ($app) {
return new class extends BypassReadonly {
protected function shouldBypass(Model $model) {
return $model->exists && auth()->can('edit', $model);
}
};
});
}
Attribute-Level Bypass Extend to bypass specific attributes:
bypass_readonly($model)->only(['name', 'email']);
Database-Level Bypass
For raw queries, use the QueryBuilder extension:
DB::connection()->enableReadonlyBypass();
DB::table('users')->update([...]);
DB::connection()->disableReadonlyBypass();
How can I help you explore Laravel packages today?