Zend-config for all your configuration needs
Different applications and frameworks have different opinions about how configuration should be created. Some prefer XML, others YAML, some like JSON, others like INI, and some even stick to the JavaProperties format; in Zend Framework, we tend to prefer PHP arrays, as each of the other formats essentially get compiled to PHP arrays eventually anyways.
At heart, though, we like to support developer needs, whatever they may be, and, as such, our zend-config component1 provides ways of working with a variety of configuration formats.
Installation
zend-config is installable via Composer:
$ composer require zendframework/zend-config
The component has two dependencies:
- zend-stdlib2, which provides some capabilities around configuration merging.
- psr/container3, to allow reader and writer plugin support for the configuration factory.
Latest version
This article covers the most recently released version of zend-config, 3.1.0, which contains a number of features such as PSR-11 support that were not previously available. If you are using Zend Framework MVC layer, you should be able to safely provide the constraint
^2.6 || ^3.1
, as the primary APIs remain the same.
Retrieving configuration
Once you've installed zend-config, you can start using it to retrieve and access
configuration files. The simplest way is to use Zend\Config\Factory
, which
provides tools for loading configuration from a variety of formats, as well as
capabilities for merging.
If you're just pulling in a single file, use Factory::fromFile()
:
use Zend\Config\Factory;
$config = Factory::fromFile($path);
Far more interesting is to use multiple files, which you can do via
Factory::fromFiles()
. When you do, they are merged into a single
configuration, in the order in which they are provided to the factory. This is
particularly interesting using glob()
:
use Zend\Config\Factory;
$config = Factory::fromFiles(glob('config/autoload/*.*'));
This method supports a variety of formats:
- PHP files returning arrays (
.php
extension) - INI files (
.ini
extension) - JSON files (
.json
extension) - XML files (using PHP's
XMLReader
;.xml
extension) - YAML files (using ext/yaml, installable via PECL;
.yaml
extension) - JavaProperties files (
.javaproperties
extension)
This means that you can choose the configuration format you prefer, or mix-and-match multiple formats, if you need to combine configuration from multiple libraries!
Configuration objects
By default, Zend\Config\Factory
will return PHP arrays for the merged
configuration. Some dependency injection containers do not support arrays as
services, however; moreover, you may want to pass some sort of structured object
instead of a plain array when injecting dependencies.
As such, you can pass a second, optional argument to each of fromFile()
and
fromFiles()
, a boolean flag. When true
, it will return a
Zend\Config\Config
instance, which implements Countable
, Iterator
, and
ArrayAccess
, allowing it to look and act like an array.
What is the benefit?
First, it provides property overloading to each configuration key:
$debug = $config->debug ?? false;
Second, it offers a convenience method, get()
, which allows you to specify a
default value to return if the value is not found:
$debug = $config->get('debug', false); // Return false if not found
This is largely obviated by the ??
ternary shortcut in modern PHP versions,
but very useful when mocking in your tests.
Third, nested sets are also returned as Config
instances, which gives you the
ability to use the above get()
method on a nested item:
if (isset($config->expressive)) {
$config = $config->get('expressive'); // same API!
}
Fourth, you can mark the Config
instance as immutable! By default, it acts
just like array configuration, which is, of course, mutable. However, this can
be problematic when you use configuration as a service, because, unlike an
array, a Config
instance is passed by reference, and changes to values would
then propagate to any other services that depend on the configuration.
Ideally, you wouldn't be changing any values in the instance, but
Zend\Config\Config
can enforce that for you:
$config->setReadOnly(); // Now immutable!
Further, calling this will mark nested Config
instances as read-only as well,
ensuring data integrity for the entire configuration tree.
Read-only by default!
One thing to note: by default,
Config
instances are read-only! The constructor accepts an optional, second argument, a flag indicating whether or not the instance allows modifications, and the value isfalse
by default. When you use theFactory
to create aConfig
instance, it never enables that flag, meaning that if you return aConfig
instance, it will be read-only.If you want a mutable instance from a
Factory
, use the following construct:
use Zend\Config\Config; use Zend\Config\Factory; $config = new Config(Factory::fromFiles($files), true);
Including other configuration
Most of the configuration reader plugins also support "includes": directives within a configuration file that will include configuration from another file. (JavaProperties is the only configuration format we support that does not have this functionality included.)
For instance:
INI files can use the key
@include
to include another file relative to the current one; values are merged at the same level:webhost = 'www.example.com' @include = 'database.ini'
For XML files, you can use XInclude:
<?xml version="1.0" encoding="utf-8"> <config xmlns:xi="http://www.w3.org/2001/XInclude"> <webhost>www.example.com</webhost> <xi:include href="database.xml"/> </config>
JSON files can use an
@include
key:{ "webhost": "www.example.com", "@include": "database.json" }
YAML also uses the
@include
notation:webhost: www.example.com @include: database.yaml
Choose your own YAML
Out-of-the-box we support the YAML PECL extension for our YAML support. However, we have made it possible to use alternate parsers, such as Spyc or the Symfony YAML component, by passing a callback to the reader's constructor:
use Symfony\Component\Yaml\Yaml as SymfonyYaml;
use Zend\Config\Reader\Yaml as YamlConfig;
$reader = new YamlConfig([SymfonfyYaml::class, 'parse']);
$config = $reader->fromFile('config.yaml');
Of course, if you're going to do that, you could just use the original library,
right? But what if you want to mix YAML and other configuration with the
Factory
class?
There are two ways to register new plugins. One is to create an instance and register it with the factory:
use Symfony\Component\Yaml\Yaml as SymfonyYaml;
use Zend\Config\Factory;
use Zend\Config\Reader\Yaml as YamlConfig;
Factory::registerReader('yaml', new YamlConfig([SymfonyYaml::class, 'parse']));
Alternately, you can provide an alternate reader plugin manager. You can do that
by extending Zend\Config\StandaloneReaderPluginManager
, which is a barebones
PSR-11 container for use as a plugin manager:
namespace Acme;
use Symfony\Component\Yaml\Yaml as SymfonyYaml;
use Zend\Config\Reader\Yaml as YamlConfig;
use Zend\Config\StandaloneReaderPluginManager;
class ReaderPluginManager extends StandaloneReaderPluginManager
{
/**
* @inheritDoc
*/
public function has($plugin)
{
if (YamlConfig::class === $plugin
|| 'yaml' === strtolower($plugin)
) {
return true;
}
return parent::has($plugin);
}
/**
* @inheritDoc
*/
public function get($plugin)
{
if (YamlConfig::class !== $plugin
&& 'yaml' !== strtolower($plugin)
) {
return parent::get($plugin);
}
return new YamlConfig([SymfonyYaml::class, 'parse']);
}
}
Then register this with the Factory
:
use Acme\ReaderPluginManager;
use Zend\Config\Factory;
Factory::setReaderPluginManager(new ReaderPluginManager());
Processing configuration
zend-config also allows you to process a Zend\Config\Config
instance and/or
an individual value. Processors perform operations such as:
- substituting constant values within strings
- filtering configuration data
- replacing tokens within configuration
- translating configuration values
Why would you want to do any of these operations?
Consider this: deserialization of formats other than PHP cannot take into account PHP constant values or class names!
While this may work in PHP:
return [
Acme\Component::CONFIG_KEY => [
'host' => Acme\Component::CONFIG_HOST,
'dependencies' => [
'factories' => [
Acme\Middleware\Authorization::class => Acme\Middleware\AuthorizationFactory::class,
],
],
],
];
The following JSON configuration would not:
{
"Acme\\Component::CONFIG_KEY": {
"host": "Acme\\Component::CONFIG_HOST"
"dependencies": {
"factories": {
"Acme\\Middleware\\Authorization::class": "Acme\\Middleware\\AuthorizationFactory::class"
}
}
}
}
Enter the Constant
processor!
This processor looks for strings that match constant names, and replaces them
with their values. Processors generally only work on the configuration values,
but the Constant
processor allows you to opt-in to processing the keys as
well.
Since processing modifies the Config
instance, you will need to manually
create an instance, and then process it. Let's look at that:
use Acme\Component;
use Zend\Config\Config;
use Zend\Config\Factory;
use Zend\Config\Processor;
$config = new Config(Factory::fromFile('config.json'), true);
$processor = new Processor\Constant();
$processor->enableKeyProcessing();
$processor->process($config);
$config->setReadOnly();
var_export($config->{Component::CONFIG_KEY}->dependencies->factories);
// ['Acme\Middleware\Authorization' => 'Acme\Middleware\AuthorizationFactory']
This is a really powerful feature, as it allows you to add more verifications and validations to your configuration files, regardless of the format you use.
In version 3.1.0 forward
The ability to work with class constants and process keys was added starting with the 3.1.0 version of zend-config.
Config all the things!
This post covers the parsing features of zend-config, but does not even touch on another major capability: the ability to write configuration! We'll leave that to another post.
In terms of configuration parsing, zend-config is simple, yet powerful. The ability to process a number of common configuration formats, utilize configuration includes, and process keys and values means you can highly customize your configuration process to suit your needs or integrate different configuration sources.
Get more information from the zend-config documentation4.
Footnotes
1. https://docs.zendframework.com/zend-config/ ↩
2. https://docs.zendframework.com/zend-stdlib/ ↩
3. https://github.com/php-fig/container ↩
4. https://docs.zendframework.com/zend-config/ ↩