Config classes

A module's configuration API, the calls other modules make to plug into it, lives on a <Module>Config class. The module's runtime behaviour stays on the <Module> class.

Two classes, two jobs

The split is fixed. ShopConfig is where other modules register with Shop. Shop is where Shop does its work at runtime. Keeping the registration API on its own class means another module can configure Shop by calling ShopConfig, and never needs to touch Shop's runtime.

Declarative registration

A config class offers declarative registration. It often has a Type() method that opens a type and runs a closure in which the caller declares the details. MemberFolderConfig works this way, and a Shop config for product types follows the same shape:

final class ShopConfig
{
    /** @var array<string,object> type key => record */
    static array $types = [];

    private static ?object $current = null;

    static function Type(string $moduleName, string $productType, callable $declare): void
    {
        Security::Str($moduleName);
        Security::Str($productType);
        Security::Callback($declare);

        $key = $moduleName.'/'.$productType;
        if (isset(self::$types[$key])) {
            return;
        }

        $record = (object)[ 'module' => $moduleName, 'type' => $productType, 'fields' => [] ];
        self::$types[$key] = $record;

        $previous      = self::$current;
        self::$current = $record;
        try {
            $declare();
        } finally {
            self::$current = $previous;
        }
    }
}

Another module plugs in

A module registers with Shop by calling ShopConfig::Type from its own index file. Inside the closure it declares what its product type holds. This is how one module plugs into another, following MemberFolderConfig::Type:

//  in Subscription/index.php
ShopConfig::Type('Subscription', 'plan', function() {
    ShopTypeConfig::Field('interval', 'Billing interval');
    ShopTypeConfig::Field('seats', 'Included seats');
});

Shop reads its registry when it needs to; the Subscription module never touches Shop's internals. Each side knows only the config API between them. See Loose coupling.

Next: Loose coupling and extension.