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 .ini s?! /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:

A Note about Booleans:



As far as I can tell, boolean values are not easy to restore, as PHP gives us an empty string ( "" ) for false and 1 for true , 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 use 1 and 0 for true and false 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 and always 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)

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)

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!