How to Simplify Expressive Configuration with Interop-Config

Zend Expressive (and Zend Framework) are great frameworks, ones designed not to constrain you in almost any way. You’re in charge. You set the scene. You make it do just what you want it to do.

Unlike other frameworks, you’re not bound to work with a specific way. You’re free to work in, almost, whatever way you want.

But that comes at a price.

Consequently, using Zend Expressive can give you too much freedom — especially when it comes to configuration. That’s why I was happy to hear about Interop-Config some time ago from my friend Sandro Keil.

Interop-Config is a library which helps ensure that you have a valid configuration for your code. It can provide default options, as well as enforce mandatory options, ensuring that it has a well laid out structure, and is easy to understand.

Sound like a good choice? Well, if you can answer yes to any of these questions, it might be for you:

  • Want to configure your factories?
  • Want to reduce your factory boilerplate code?
  • Want to check automatically for mandatory options or merge default options?
  • Want to have a valid config structure?
  • Want to generate your configuration files from factory classes?

In this tutorial, I’m going to step you through integrating it into a Zend Expressive application, so that you get a hands-on feel for how it works.

How to Install Interop-Config

I guess it’s fair to expect that the first thing we’re going to do is to install it, and that we’re going to do so via Composer.

To do that, the best way to do it is to use Composer’s require command, as in the following example. This way you don’t have to mess with composer.json yourself.

composer require sandrokeil/interop-config

After a few minutes, if that, Interop-Config will be available as a dependency in your project; so we’re ready to begin.

A Sample Configuration

The base configuration which we’ll work with in this tutorial you can see below. I commonly use ConfigProvider classes for all code configuration. But that doesn’t make for a brief example. So, here’s what it would look like if I didn’t:

<?php

return [
  'app' => [
      'authentication' => [
          'default_redirect_to' => '/',
      ]
  ]
];

In this example, under the app key, I have another array, called authentication, which contains authentication settings.

The sole option indicates where the login process should redirect to after a successful login occurs. It’s part of a larger application I’ve been working on lately.

We could have a lot more options, but we don’t need to.

How to Use It In a Class

What I’m going to do in the code examples in this tutorial, is to assume that I want to simplify the configuration of a factory class, which is responsible for instantiating a LoginPage action class.

What the class being instantiated does, isn’t that important. But you can assume that it will render a login form where a user can enter a username and password.

The first thing that we’re going to do is to have the class implement the required interface. There are two choices available:

  • ReqiresConfig: This interface stipulates that a class requires a configuration
  • ReqiresConfigId: This interface, like RequiresConfig, stipulates that a class requires a configuration. But it allows for more than one instance, with different configurations per/instance

I’m going to use RequiresConfigId. So the essentials of my class will look like the example below.

<?php

namespace App\Action;

use Interop\Config\ConfigurationTrait;
use Interop\Config\RequiresConfig;
use Interop\Config\RequiresConfigId;

class LoginPageFactory implements RequiresConfigId
{
  use ConfigurationTrait;
}

I’ve made use of the ConfigurationTrait. This trait has several utility methods which make working with configurations simpler, which we’ll see throughout the remainder of the post.

With that done, we now have to implement the required methods, only one, dimensions(). Let’s do that now.

<?php
public function dimensions()
{
  return ['app'];
}

What this code sample will do is ensure that the application configuration needs to contain a key with the value app. We could add in any number of keys here, so our configurations could become rather complex. I’ve not done so for sakes of simplicity.

How to Retrieving Configuration Data

Now let’s flesh out the code to make use of it the configuration. In the code below, you can see that I’ve called the options() method, passing in two parameters.

The first parameter is the application’s configuration, retrieved from the DI container. The second is the element within our app configuration, which we want to access.

<?php
public function __invoke(ContainerInterface $container)
{
  // retrieve other dependencies from the container...

  // retrieve configuration from the container
  $authenticationOptions = $this->options(
    $container->get('config'),
    'authentication'
  );

  return new LoginPageAction(
    $router,
    $template,
    $userRepository,
    $userEntity,
    $authenticationOptions['default_redirect_to']
  );
}

By using the options method it automatically filters down on the section of the configuration identified by the array we returned in the dimensions method.

This has two benefits. Firstly, we don’t need to do as much work to access the relevant section of the configuration which we need.

Secondly, it ensures that it’s there, and throws one of four exceptions if it’s not found, or doesn’t contain the information which we need.

How to Provide Default Options

Now, what if we wanted to provide default options, options which could be overridden later. What if we wanted to provide default options such as the status code to use for a redirect?

To do that, we’d have to implement the ProvidesDefaultOptions interface, and implement the defaultOptions() method.

Assuming that we’d included Interop\Config\ProvidesDefaultOptions via a use statement at the top of our class file, here’s what the method would look like.

<?php
public function defaultOptions()
{
  return [
    'status_code' => 302,
  ];
}

Here, we’re providing a default option of status_code, using the de facto redirect status code of 302. If the user wanted to use another code later, they could set this option in their configuration.

These kinds of options, like standard ports and hostnames for databases, image quality levels, sound recording levels and so on are good candidates for having default options provided.

How to Make Options Mandatory

Mandatory options go hand-in-hand with default options. While some values can have sane defaults provided for them, others need to be specified.

These include such options as authentication credentials and business operating hours. Given that, we need a way to enforce that the user provides them. Interop-Config handles this through the RequiresMandatoryOptions interface.

To do so, we’d include use Interop\Config\RequiresMandatoryOptions; at the top of our class file, and implement the mandatoryOptions() method, which you can see below. This method, as with defaultOptions(), ensures that the settings specified are present in the default configuration.

When I was initially working with the library, I thought (for some reason) that default options could satisfy mandatory options. Please don’t get caught out like I did. Default options won’t meet mandatory ones.

public function mandatoryOptions()
{
  return [
    'default_redirect_to'
  ];
}

Here, we’re forcing the user to provide default_redirect_to in the authentication element of the provided configuration. If it was not present, you’d likely see output similar to that below, where a ServiceNotCreatedException was thrown.

This was the result of an Interop\Config\Exception\MandatoryOptionNotFoundException being thrown, as the configuration was missing.

Interop Config (by Sandro Keil) - Mandatory Option Not Found Exception

For what it’s worth, in this example the name default_redirect_to is a misnomer and perhaps even confusing. It would likely be a default property, not a mandatory one. But for the purposes of an example, it works.

What About Defensive Programming?

All of this is well and good, but what about being programming defensively? What about being able to check, ahead of time, if a configuration is even present, before attempting to use it?

To do this, you need to make use of the canRetrieveOptions() method. As the name implies, it checks if the configuration options you need have been registered in the application’s configuration.

if ($this->canRetrieveOptions($container->get('config'), 'authentication')) {
  $authenticationOptions = $this->options($container->get('config'), 'authentication');
}

The example above demonstrates how to use it. One thing worth noting, however, it only checks if an element by the name supplied exists in the configuration.

If it returns true, and you attempt to retrieve the configuration — but — it doesn’t contain the mandatory options, then an exception will still be thrown.

Perhaps you’re like me and, at face value, inferred that the method would check if the configuration satisfied both conditions. It doesn’t. I’m not blaming the library, nor critiquing it. This is just to be sure that you know.

In Conclusion (tl;dr)

And that’s Interop-Config. I’ve not used it extensively yet. But from what I have, I’m very happy with it. Whether it’s because I live in Germany, or I just generally like structure, this library suits me to a tea. What about you? Fancy giving it a try in your applications?



About Matthew

Matthew Setter Matthew Setter is a PHP & Zend Framework specialist. If you're in need of a custom software application, need to migrate an existing legacy application, or want to know your current application's GPA - get in touch.