You can tell I'm a huge fan PHP configs. To be honest, I don't care; I'm just extremely lazy.
Yet, my laziness got me itching when I see configuration of extensions.
I like the service configuration provided by Symfony. Typo-proof, everything is autocompleted by IDE, hard to put the wrong argument or make a typo.
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
return function (ContainerConfigurator $containerConfigurator): void {
$services = $containerConfigurator->services();
$services->defaults()
->autowire()
->autoconfigure()
->public();
$services->set(SomeService::class)
->args(['some', 'value']);
};
This config is a joy to use in IDE. Only the values that can change, like class name or arg value, are strings. Everything else is API of the modeling tool, here ContainerConfigurator
from Symfony. This code is a state of Art.
But that's not everything we have in our configs. Let's look at a common extension you can found in config/packages/doctrine.php
in your Symfony project:
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
return function (ContainerConfigurator $containerConfigurator): void {
$containerConfigurator->extension('break', [
'dbal' => [
'host' => '%env(DATABASE_HOST)%',
'user' => '%env(DATABASE_USER)%',
'password' => '%env(DATABASE_PASS)%',
],
]);
};
How do you like this?
I'll share you secret deep from my traumatized mind - this is what I see:
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
return function (ContainerConfigurator $containerConfigurator): void {
$containerConfigurator->extension('error', [
'bug' => [
'typo' => ' _missing',
' problem ' => 'sorry_renamed',
'forgotten' => 'FALSE POSITIVE',
],
]);
};
"Anything that can go wrong, will go wrong.
In the worst possible order. When you least expect it."
If we look at ContainerConfigurator
, it inherits from abstract class AbstractConfigurator
.
What if we use per-extension Configurator... e.g. DoctrineConfigurator
?
return static function (DoctrineConfigurator $doctrineConfigurator): void {
$doctrineConfigurator->dbal()
->user('%env(DATABASE_USER)%')
->password('%env(DATABASE_PASS)%');
};
Slightly less space for a bug... Still, I managed to split one there. It can cause a "connection to database rejected" error.
Have you spotted it?
return static function (DoctrineConfigurator $doctrineConfigurator): void {
$doctrineConfigurator->dbal()
->user('%env(DATABASE_USER)%')
- ->password('%env(DATABASE_PASS)%');
+ ->password('%env(DATABASE_PASSWORD)%');
};
Jan Mikes shared with me an interesting idea that removes this problem:
use DoctrineEnvs;
return static function (DoctrineConfigurator $doctrineConfigurator): void {
$doctrineConfigurator->dbal()
->user('%env(' . DoctrineEnvs::DATABASE_PASSWORD . ')%')
->password('%env(' . DoctrineEnvs::DATABASE_PASSWORD . ')%');
};
That seems like too much clutter... can we make it simpler and safes?
use DoctrineEnvParams;
return static function (DoctrineConfigurator $doctrineConfigurator): void {
$doctrineConfigurator->dbal()
->user(DoctrineEnvParams::DATABASE_PASSWORD)
->password(DoctrineEnvParams::DATABASE_PASSWORD);
};
In what cases is this the most useful?
Common key for the "database name" is never the same across database platforms and language integration. Imagine the saved hours and lives on Docker/Doctrine/CI typos:
return static function (DoctrineConfigurator $doctrineConfigurator): void {
$doctrineConfigurator->dbal()
->database('%env(DATABASE_NAME)%');
// or...?
->database('%env(DATABASE_DBNAME)%');
// or...?
->database('%env(DATABASE_DATABASE)%');
// or...?
->database('%env(DB_NAME)%');
};
Just don't care and use IDE autocomplete:
use DoctrineEnvParams;
return static function (DoctrineConfigurator $doctrineConfigurator): void {
$doctrineConfigurator->dbal()
->database(DoctrineEnvParams::DATABASE_NAME);
};
int
, the PHP throws an exception right on that linedbal()
or orm()
you only get methods, that are relevant in that contextreturn static function (DoctrineConfigurator $doctrineConfigurator): void {
$doctrineConfigurator->dbal()
->... // dbal specific methods
$doctrineConfigurator->orm()
->... // orm specific methods
};
connection()
return static function (DoctrineConfigurator $doctrineConfigurator): void {
$doctrineConfigurator->dbal()
->conntection()
->... // only connection specific methods
};
The DoctrineConfigurator
with specific methods()
and Constant::KEYS
is one way to get rid of all string possible.
With PHP 8.0 to be released in 11/2020 will come named arguments. With them, the IDE autocomplete becomes more powerful. If we combine it along with __construct
validation in value objects, we have another solid way to add parameters:
return static function (DoctrineConfigurator $doctrineConfigurator): void {
$doctrineConfigurator->dbal()
->connection(new DbalConnection(
DoctrineEnvParams::DATABASE_USER,
DoctrineEnvParams::DATABASE_PASSWORD,
DoctrineEnvParams::DATABASE_NAME
));
};
Compared to method() autocomplete, we can also see what arguments are required and which optional:
// using PHP 8.0 syntax with constructor promotion
final class DbalConnection
{
public function __construct(
private string $user,
private string $password,
private string $database,
private ?string $version = null
) {
// ...
}
}
If we get rid of strings that can go wrong, we've made a big shift to senior codebase.
Another huge benefit for programmers is focus - along with instant feedback loop.
"If something goes wrong, I want to know it the moment it went wrong"
If we put the wrong password to the database, the project works on the localhost server, the tests are passing, and CI is green, our feedback loop is slow, and we have to speed it up.
If the project crashes on the localhost server and tests are failing, our feedback look is fast.
How is instant feedback loop related to value objects and require arguments? Good question!
If we put user and password but forget the database name, the application will crash when it's connected:
use DoctrineEnvParams;
return static function (DoctrineConfigurator $doctrineConfigurator): void {
$doctrineConfigurator->dbal()
->user(DoctrineEnvParams::DATABASE_USER)
->password(DoctrineEnvParams::DATABASE_PASSWORD);
};
That's very late!
With value objects, we'll get the "missing 3rd argument" error:
return static function (DoctrineConfigurator $doctrineConfigurator): void {
$doctrineConfigurator->dbal()
->connection(new DbalConnection(
DoctrineEnvParams::DATABASE_USER,
DoctrineEnvParams::DATABASE_PASSWORD
// boom :(
));
};
That's fast feedback loop!
These were few ideas, how to get rid of strings in /config
directory, so we can instead focus on something we love - algorithms, coding, and maybe a coffee cup!
I bet you can't come up with a better way to do this... share in comments to prove me wrong ;)
Happy coding!
Do you learn from my contents or use open-souce packages like Rector every day?
Consider supporting it on GitHub Sponsors.
I'd really appreciate it!