What about config builders?
Written on 2021-06-18
I've been tinkering with a hobby project lately: a small framework to get more familiar with PHP 8, and try out some random ideas floating in my head. It's nothing too serious, but it's a fun exercise.
Most of these ideas are born from my daily experience with Laravel, and more specifically from the little annoyances I have with it. Now, don't get me wrong: I think Laravel is one of the best frameworks out there when it comes to modern PHP development and it's only natural that it has a quirk here and there, after a decade of development.
So this definitely isn't a Laravel-rant, rather it's just a thought experiment in dealing with one of those little annoyances.
So, Laravel has these PHP config files, right. Here's one example (this one is auth.php, if you're wondering):
<?php return [ 'defaults' => [ 'guard' => 'web', 'passwords' => 'users', ], 'guards' => [ 'web' => [ 'driver' => 'session', 'provider' => 'users', ], 'api' => [ 'driver' => 'token', 'provider' => 'users', 'hash' => false, ], ], 'providers' => [ 'users' => [ 'driver' => 'eloquent', 'model' => App\Models\User::class, ], ], 'passwords' => [ 'users' => [ 'provider' => 'users', 'table' => 'password_resets', 'expire' => 60, 'throttle' => 60, ], ], 'password_timeout' => 10800, ];
While I think PHP configuration files are far superior to, for example, YAML or XML configuration; there's one thing that annoys me quite a lot with it: there are no IDE insights in what kind of config values are available in this array, let alone which type of values they require.
To counteract this problem, the official Laravel config files have these inline doc blocks explaining each config entry. Most serious third party packages also provide such doc blocks.
With PHP 8 and named arguments though, there's a better solution available: config objects that know exactly what kind of data they'd need.
Here's what I'd imagine the auth.php would look like with it:
return AuthConfig::make() ->defaults( guard: 'web', passwords: 'users', ) ->guards( web: GuardConfig::make() ->driver('session') ->provider('users'), api: GuardConfig::make() ->driver('token') ->provider('users') ->hash(false), ) ->providers( users: AuthProviderConfig::make() ->driver('eloquent') ->model(User::class), ) ->passwords( users: PasswordConfig::make() ->provider('users') ->table('password_resets') ->expire(60) ->throttle(60) ) ->passwordTimeout(10800);
Thanks to named arguments and their support for variadic functions, we end up with a conciser syntax, while still having all documentation available to us: it's added as property types and doc blocks in these config objects, instead of being hard coded in the config files as text.
To me that's the most important value: your IDE tells you what you need to do, instead of having to read documentation — inline or external:

The only thing needed for this to work is some kind of interface that requires these config builders, as I like to call them, to implement a toArray method. You could go one step further and turn things around by always using config objects instead of arrays, which would allow you to also make use of their built-in documentation when reading config, and not only when initializing it. That's a bit more of a aggressive change though.
Here's what a config builder implementation would look like:
class AuthConfig extends ConfigBuilder { public function defaults( ?string $guard = null, ?string $password = null, ): self { $this->config['defaults']['guard'] = $guard ?? $this->config['defaults']['guard'] ?? null; $this->config['defaults']['password'] = $password ?? $this->config['defaults']['password'] ?? null; return $this; } public function guards(GuardConfig ...$guardConfigs): self { foreach ($guardConfigs as $name => $guardConfig) { $this->config['guards'][$name] = $guardConfig->toArray(); } return $this; } public function passwordTimeout(int $timeout): self { $this->config['password_timeout'] = $timeout; return $this; } }
Another improvement I can come up with is by using enums instead of string values. They will be natively available in PHP 8.1, but there are also alternatives out there for older PHP versions.
Let's just assume we're already running PHP 8.1, we could write parts of it like so:
->guards( web: GuardConfig::make() ->driver(Driver::Session) ->provider(Provider::Users), api: GuardConfig::make() ->driver(Driver::Token) ->provider(Provider::Users) ->hash(false), )
These ideas aren't new, by the way. We've been using config objects in some of our packages at Spatie, but we always start from a simple array and convert it when booting the application. There's also PHP CS that uses the same approach. Also, Symfony just recently added support for a fluent interface to build their config files; great!
Join over 14k subscribers on my mailing list: I write about PHP, programming, and keep you up to date about what's happening on this blog. You can subscribe by sending an email to [email protected].
There's one advantage to tinkering with your own framework: you're not constrained by backwards compatibility and legacy. I imagine it might not be trivial to properly support config builders in Laravel, at least not as the default approach. They also become much less useful if you cannot use named arguments, which require PHP 8.
But, who knows? Maybe something similar might get added in the future in Laravel? Or maybe some third-party packages start doing it on their own first. Anyway, I'm going to tinker some more with my custom framework, just for fun!
What's your opinion on config builders? Let me know your thoughts, via Twitter or by subscribing to my newsletter.
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.