Table of Contents
I was reviewing a service provider that registered a class by newing it up and then setting properties on it with extend(). It worked, but it was fragile β properties could be overwritten later, and you couldn’t make them readonly.
The Before
// AppServiceProvider.php
$this->app->bind(NotificationPlugin::class, function ($app) {
$plugin = new NotificationPlugin();
$plugin->apiKey = config('services.notify.key');
$plugin->endpoint = config('services.notify.url');
$plugin->timeout = 30;
return $plugin;
});
This pattern has a few problems:
- Properties are mutable β anything can overwrite
$plugin->apiKeylater - No way to use PHP 8.1’s
readonlykeyword - If you forget to set a property, you get a runtime error instead of a constructor error
- Hard to test β you need to set up each property individually in tests
The After
class NotificationPlugin
{
public function __construct(
public readonly string $apiKey,
public readonly string $endpoint,
public readonly int $timeout = 30,
) {}
}
// AppServiceProvider.php
$this->app->bind(NotificationPlugin::class, function ($app) {
return new NotificationPlugin(
apiKey: config('services.notify.key'),
endpoint: config('services.notify.url'),
timeout: 30,
);
});
What You Get
Immutability. Once constructed, the object can’t be modified. readonly enforces this at the language level.
Fail fast. If you forget a required parameter, PHP throws a TypeError at construction time β not some random null error 200 lines later.
Easy testing. Just new NotificationPlugin('test-key', 'http://localhost', 5). No setup ceremony.
Named arguments make it readable. PHP 8’s named parameters mean the service provider binding reads like a config file.
The Rule
If you’re setting properties on an object after construction in a service provider, refactor to constructor injection. It’s more explicit, more testable, and lets you use readonly. Your future self will thank you when debugging a “how did this property change?” mystery.

Leave a Reply