Over the past weekend, I discovered an undocumented (and what I think is a super
cool) feature in PHP’s parse_ini_file
and parse_ini_string
functions. If you
assign to a field an environment variable, it will populate the resulting
array with it’s value!
I stumbled upon this while throwing together a configuration system for an API I
am working on (no framework, just libraries and single component–packages). When
I landed on using .ini
files instead of .php
files, one of the first
questions I had was
If I am unable to use functions for populating variable config values, how am I going to handle injecting environment variables into the config for caching the aggregate copy later?
A few ideas passed, and I jotted them down, still yet wanting an even simpler solution… you know; the whole “the best code is the code not written” thing and all. I don’t know what possessed me to ask the next question…
What if I just assign a Bash interpolated variable? Will PHP even parse that, or will it just throw an error back at me? ¯\_(ツ)_/¯ I guess YOLO?
but I sure am glad that I did. I bet the look on my face after seeing it work,
was of shock & utter disbelief; the kind where you aren’t sure if you just made
up what you saw in your head, and have to do a double-take to be sure. But I
digress, we aren’t talking about that here today, instead we are talking about
the awesome simplicity of using .ini
files over the plethora of other
options available to PHP programs, including but not limited to,
- XML
- YAML (seriously, why? ヽ(°〇°)ノ )
- JSON
- PHP
Before moving on, I must give credit where credit is due. After I made this discovery on my own, of course I Googled it to see if this was in the documentation. What I did find, is a comment from someone that mentioned it, nine years ago. Great!
Let’s get an example going.
Okay Finally, an Example
Assume we have the following .ini
file. We will call it example.ini
,
version=1.0.0
[section]
foo=${BAR}
Next, in our terminal, let’s set the BAR
environment variable and parse the
file!
BAR='Hello, World!' php -r "print_r(parse_ini_file('./example.ini', true));"
This will (at least, it should…) yield us the following array as output.
[
'version' => '1.0.0',
'section' => [
'foo' => 'Hello, World!',
],
];
Crazy right?! Okay, maybe not. But you might be asking yourself if this works
with the community “dotenv” packages from
vlucas or
Symfony, so that you don’t have to source
the .env
file manually in your bootstrapping process, and the answer is
yes; you bet it does!
If I am being completely honest, this hidden feature alone has pretty much made
me abandon .php
configuration files entirely, at least for my personal
projects where developer on-boarding is not a crucial element to design
decisions, such as configuration file format. It sounds a little bold, but it
gets even better (keep reading)!
But why not just use <INSERT_FORMAT_HERE>
instead?
I think (in fact, I am pretty positive) that I hear some groans coming from some of you reading this right now. I know… you’re probably thinking,
.ini
files? ew, those are old and ugly.
or perhaps,
But they are used to configure PHP! Using them in an app would be weird.
maybe even,
How do I use PHP features in my configs then? I guess I have to create a DSL to use within the
.inis
?! /scoffs
what about,
Types are lost and not given to us by the functions!
or something else. Perhaps none at all, and you agree with me. Whatever it is,
here are my reasons for actually liking the .ini
configuration file instead of
the other widely used formats (we will get to the value types later).
1) It’s actually a configuration format
I guess this is could be nitpicking and just being semantic for no reason, but hear me out: XML is a markup language, enough said. JSON and YAML are data-interchange (serialization) formats. Yes, they are used as configuration, and more suitable than markup, but that’s not their intended purpose/use-case. The opposite is true for the INI format, which is specifically an initialization/configuration format.
Using PHP files that return an array as configuration is perfectly fine. My
issue with it however, is that you can get very tempted to start calling
functions here and there, you know.. one here won’t hurt… maybe spice it up
next time with some logic to set a value; maybe get real saucy and make some
database calls? Pretty soon, you’re left with a lot of code in your
configuration. This is not always a bad thing though (for example, using
getenv
or similar to read environment specific values and maybe secrets), in
fact some even prefer the idea of code over configuration, I totally get it,
but for me it can become cumbersome, especially in team settings.
2) Simple to type, forgiving, and easy on the eyes
Writing JSON is infinitely nicer than YAML, and writing PHP arrays as configs can get really monotonous when you’ve got large amounts of things to enter, with various levels of nesting, etc. XML is just nasty.
Now INI files on the other hand, these babies are quick to write, easy to
visually grep, and in my opinion, actually quite nice to look at when formatted
with care. Blank lines and (most) white space is ignored, so the parse_ini_*
functions are fairly resilient with what goes through it.
3) Keeps configuration with configuration, and code with code
Separation of concerns is really what it comes down to for this one. Personally, I believe that your configurations should be utterly stupid and have no way to interact with the application. As in, they are read only, speak only when spoken to, and get written to at build time (cache warmup, etc) for injecting any environment-specific variables.
Whether I have convinced you, intrigued you, or maybe you aren’t quite on board, I thank you for sticking with me so far.
The implementation so far, while cool, leaves a lot to be desired. Next, we will
talk about improving the built-in parse_ini_*
functions, and even add some
open-source package(s) to make a strong configuration system that you can
utilize in any project.
Taking it a Step Further
PHP’s parse_ini_*
functions are unable to properly cast types, nor does it
support array literals (lists). It also does not support nesting of properties,
or section inheritance; all of which are pretty nice features to have when
managing configurations in a program. That’s not to say that we can’t add
support for it though…
INI 2.0, the Future
Thinking about the project I am working on’s requirements, I thought,
I am probably going to want some kind of way to nest keys, have array literals, and get proper scalar types back. Wrangling them in the application code would be a fucking nightmare.
As you might have guessed, it turns out to be not much work to whip up a decent
“complex” .ini
parser that can (mostly – more on this in a bit) properly cast
values to the correct types and even implement nested properties & section
inheritance. A quick Google will yield some nice StackOverflow answers.
Let’s look at an example of what these might look like.
Our .env
file will contain a single variable which is used by the config to
determine which stage/server the application should use.
APP_STAGE=dev
The configuration file, servers.ini
contains some sections for each “stage” the
application might be in, containing the server address information required to
connect, maybe some metadata as well. There is a top level config key, stage
.
This key will read from our environment variable to receive it’s value during
runtime.
provider=aws
stage=${APP_STAGE}
[dev]
protocol=http
host=localhost
port=8080
region=""
some.nested.list=[]
[staging < dev]
protocol=https
host=staging.domain.tld
port=443
region=us-west-1
some.nested.list=[1, 2]
[production < staging]
host=domain.tld
region=us-west-2
some.nested.list=[3, 'sup', 'yo', 0.5]
Dumping the parsed INI files in our application, we would expect it to yield the
following array output. And as you can see, the .ini
file is much cleaner and
compact than the PHP counterpart.
[
'servers' => [
'provider' => 'aws',
'stage' => 'dev',
'dev' => [
'protocol' => 'http',
'host' => 'localhost',
'port' => 8080,
'region' => '',
'some' => [
'nested' => [
'list' => [],
],
],
],
'staging' => [
'protocol' => 'https',
'host' => 'staging.domain.tld',
'port' => 443,
'region' => 'us-west-1',
'some' => [
'nested' => [
'list' => [
0 => 1,
1 => 2,
],
],
],
],
'production' => [
'protocol' => 'https',
'host' => 'domain.tld',
'port' => 443,
'region' => 'us-west-2',
'some' => [
'nested' => [
'list' => [
0 => 3,
1 => 'sup',
2 => 'yo',
3 => 0.5,
],
],
],
],
],
];
Having these capabilities in a mechanism to parse an .ini
file shows that the
INI format gives us a lot of room to work with, while limiting us just enough to
stay in line and have clear boundaries about what our program’s configuration is
allowed to do.
I will leave it as an exercise to you, the reader, to implement your own
parse_ini_*
wrapper that adds support for,
- Inheritance
- Nested properties
- Scalar types
If you are not interested in doing that, just want to play around, or are not fond of rolling your own stuff and prefer to use an available solution:
- I have a library available on Packagist with the function I wrote. Check it out if you’re interested in my implementation; or
Zend FrameworkLaminas offers a component for this, which might be better suited for serious, “enterprise”(?) projects.
A Note about Booleans:
As far as I can tell, boolean values are not easy to restore, as PHP gives us an empty string (
""
) forfalse
and1
fortrue
, except when the INI field’s value comes from a boolean environment variable! If this happens, you will get them as strings,"false"
or"true"
LOL! This happens because environment variables are always strings.Shitty… it seems very difficult, if not impossible to guess the intent without resorting to some kind of weird convention, like prepending the key name with “is” or some bullshit, e.g.,
isEnabled
– feels.. leaky to me. A better convention would be to only use1
and0
fortrue
andfalse
respectively, and cast them somewhere later. We can also continue to let.env
files use boolean, as those strings are easy to cast back in this same step.I’ve opted to cast them to booleans during a “sanitization” step, before normalization and validation. This is a safe abstraction, as we are writing some code which knows (actually, expects) what values configurations should be and are allowed to be. More on this later!
Performance, Validation and Normalization
The next thing that I did to improve the idea, was backing the .ini
files with
some kind of schema, or validation. I also wanted a nice way to load and cache
the file(s). It would be trivial (though monotonous) to roll my own solution, or
I could use some nice packages from the community. I opted for Symfony’s
configuration component
(remember, this project has no framework behind it) as I’ve used it before, it’s
reliable, simple, and stable.
The Loader
The first thing I did (after creating my “complex” .ini
parsing function in my
own helper library) was implement an IniFileLoader
for the Symfony component
to use. It looks just like this (ini
is my function – replace it with your
implementation of choice),
use Symfony\Component\Config\Loader\FileLoader;
final class IniFileLoader extends FileLoader
{
public function load($resource, $type = null)
{
return ini($resource);
}
public function supports($resource, $type = null)
{
return is_string($resource)
and 'ini' === pathinfo($resource, PATHINFO_EXTENSION);
}
}
Once I had that in place and wired up with Symfony’s component, I began placing
the data in an array property, $aggregate
, on a class in the application, keyed
by the config file’s name. $aggregate
is where values are read from the
application during runtime.
Caching the Aggregate
Sticking with simplicity in mind, I used the built-in cache mechanism that the component offers. Nice and easy, no fuss, no custom code to write.
I set mine up to watch all *.ini
configuration files and the .env
file for
changes. This way, any values changed during development and the config will
regenerate appropriately and repopulate the cache for the next request.
Validation and Normalization
One of the most common issues I see with configuration systems used in the wild, is there is no clear documented set of available configurations, what they all do, which ones are deprecated, etc.. This is usually (in my experience) because the configurations are haphazard PHP arrays, and/or the configurations are very large files, grown over time, often containing duplicate purpose configurations with slightly different names.
The Symfony configuration component comes in handy once more, by offering us the
ability to define a schema or “tree” as they put it for config files. The config
arrays that are output from the IniFileLoader
are processed by the config
trees and will throw exceptions if any constraints are violated, ensuring your
application never boots with missing or bad configuration values. I won’t go
into how you use it, as the documentation is plenty useful at doing that
already, and not really in the scope of this article.
Back to Booleans
Alright! It is this part of the process that I am converting config values which I know and expect to be booleans, into PHP booleans. I use the
beforeNormalization
andalways
methods in the configuration tree class, with a simple callback that returns the casted value, making sure to check for the string values"true"
or"false"
and if present, handle them accordingly.
And with that, we have a simple, clean, cacheable, schema-backed & validated configuration, ready to receive environment variables from your application’s host or container. To close out I will go over some pros and cons of this approach to configuration, as some closing thoughts to leave you with!
Pros
- Supports environment variables out of the box (PHP’s
parse_ini_*
functions) - Compact and easy to skim/write compared to other formats
- Language and operating system agnostic – good portability (e.g., moving to GoLang)
- Limits scope of configuration files to only configuration
Cons
- Configuration files are limited in ability (e.g., no usage of
::class
for storing class names) - Minor pain in the arse to parse and restore types with some small amounts of code or a small package
- Simplistic nature can make the syntax hard to work with (if you want multiline values they need to be quoted and parsed accordingly)
If I could, I would link to the code of project that lead me to using this solution, but it is not a public source code. For now, you will have to experiment on your own and see what is right for your configuration needs!