New feature: php-config directories


#1

Often you need to configure properties of a class at multiple levels, and in the past this meant creating or overriding a php-config/ClassName.config.php file. By overriding the whole file though any other configurations it contained from upstream would then be frozen in time locally, and updates from upstream could only be brought in through manual merging.

Now, rather than creating a ClassName.config.php file, you can create a ClassName.config.d directory in the same place and every .php script within it (herein called “partial config scripts”) will get executed during a class’s config phase (between when the class is loaded and when execution returns to the call that triggered the class to be loaded.)

All of skeleton-v2’s php-config code has been broken out into partial config scripts with meaningful names that are grouped as you might want to override them. But now, if you don’t want to override them, it’s easy to create a name for your config script that either reflects the package it belongs to or the feature being added. For example, the partial config scripts that merely implement skeleton-v2 modernization on top of legacy skeleton classes are just called skeleton-v2.php under their respective ClassName.config.d directories. SearchRequestHandler.config.d however has been broken out into partial config scripts: cms.php, people.php, and tags.php. It’s easy to see in this case why it’s great now that you can just add your own script to the mix to plug in additional search handler without interfering with these common ones being defined upstream.

skeleton(-v1) will be updated soon to have the same changes.

For backwards compatibility, all partial config scripts are executed before any classic ClassName.config.php script. This ensures that defaults from newly split-up class config won’t suddenly take precedence over an override you already have for their monolithic ancestor.

Under the hood

This all can be done pretty efficiently thanks to the APCU shared cache, PHP’s opcode cache, and Emergence’s file system event scripting.

After Site::loadConfig scans the php-config tree for qualified config scripts, it caches the full ordered list of VFS node IDs (including that, if any, for the classic ClassName.config.php file at the end) under a key in APCU:

    Cache::store('class-config:ClassName', [123, 124, 125, 4]);

Then when that same class needs to be loaded on subsequent requests, the array of node IDs can be pulled out of shared memory and used to execute each script in order via a statically-built file path:

        foreach ($configFileIds AS $id) {
            require(SiteFile::getRealPathByID($id));
        }

By combining PHP’s opcache with the Emergence VFS’s nature of never rewriting existing file nodes and only appending new ones, all those subsequent calls to require never have to touch a disk or parse PHP. The emergence-kernel’s php-fpm driver configures PHP with PHP’s opcache.validate_timestamps option disabled. This means that once PHP’s opcache has compiled and cached the bytecode for a given script, it never has to even ask the disk if that file has been modified because a new file or new version of an existing file would have a new path it hadn’t seen before. So PHP can execute require('/emergence/sites/my-site/data/123'); lightning fast with precompiled bytecode in shared memory.

To keep the cache fresh, a script under the event-handlers tree blows out all cache keys matching class-config:* when any event fires from any path under the php-config tree:


This script definitely is ripe for improvement – it could figure out which classes` config cache would be impacted by the given tree change and only clear those – but it’s good enough to keep development instances responsive to changes and production instances humming under load.


#2

Here are some examples of monolithic config files getting broken down into partial config scripts: