One of the design choices for configuration in Solar, and later in the Aura v1 framework, was that all values were inteded to be passed at construction time. If you wanted to set up a router, for example, you would create an array structure to define all the routes, and then the DI container would pass that structure as a constructor param when creating the router object.

In a most situations, config via constructor param is perfectly reasonable. Unfortunately, there are some times where it’s not as practical as I would like. It can feel clumsy setting up these kinds of huge data structures, such as for a router system. We wanted to find a solution to these less frequent but still necessary setup situations, perhaps using a programmatic approach.

The problem is that programmatic setup of foundational components can itself be a little clunky. The solution I see most often for this is to use static methods on the target object; for example, Router::add() to map a new route. In Aura, static methods are a design approach that we consciously avoid. We want to keep good scope separation at all times, and make all dependencies explicit. Statics are not congruent with those goals.

Two-Stage Config

After a long period of consideration, research, and experiment, we have found a non-static solution for programmatic configuration through a DI container. It is part of a two-stage configuration process, implemented through a ContainerBuilder.

The two stages are “define” and “modify”:

  • In the “define” stage, the Config object defines constructor params, setter method values, and services. This is the equivalent of the previous single-stage Solar and Aura v1 configuration system.

  • The ContainerBuilder then locks the Container so that its definitions cannot be changed, and then begins the “modify” stage. In this second stage, we retrieve service objects from the Container and modify them programmatically.

Classes, Not Includes

We originally tried do this define-and-modify process with include files, as is usually the case with configuration approaches. With this approach, we needed two config files (define.php and modify.php) for each configuration mode. We also needed an Aura.Includer to scan the file system in predefined locations for the config file pairs for each installed package.

However, that include-oriented approach never felt quite right. A couple of weeks ago it occurred to me that we could use classes for the two-stage config operations. This would allow for autoloading, independent testing, a smaller number of files, and a host of other subtle quality improvements. This led to the creation of the Config class, which is now part of the Aura.Di v2 package.

So now, instead of each Aura package carrying a pair of define and modify includes in a subdirectory named for the config mode, we have a single class file for each config mode. The class file is in a subnamespace _Config under the package namespace. Here are two examples, one from the Aura.Web_Kernel package, and one from the Aura.Web_Project package.

Because they are classes, you can call other methods as needed, subclass or inherit, use traits, create instance properties and local variables for configuration logic, and so on. You could even call include in the methods if you wanted, keeping the class as a scaffold for much larger configuration files. This gives great flexibility to the confguration system.

Composer Assistance

None of this is any good if the application cannot find and load the configuraiton logic. To make sure the Aura project installation can find all the package config files, we use the {"extra": {"aura": { ... } } } elements of the composer.json in each package to provide a mapping from the config mode to the config class. As this example from Aura.Cli shows, we accomplish this in two parts:

  • Create a PSR-4 entry for the Aura\Cli\_Config namespace that maps to the package config/ directory, and

  • Create an entry for the config mode that maps to the Config class for that mode.

This is where the “extra” entry provided by Composer really shines. We can place critical project-level information in the composer.json for each package under the “extra” entry, namespaced for “aura”. This information can be structured any way we like.

We then inject the decoded JSON data from the project-level composer.json along with the decoded JSON data from vendor/composer/installed.json into our Project service. Note that the Project object does not read the Composer data itself; that data is injected at construction time.

Finally, we use getConfigClasses() to get back that project-level information from the object, and use them to load our ContainerBuilder.

Conclusion

With this two-stage configuration mechanism, using classes instead of includes, and using Composer to map the config modes the to the config classes, we have the benefits of both a define-only config system and a programmatic modification system. This gives us the best of both worlds, and helps us avoid resorting to static methods for configuration.

blog comments powered by Disqus