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.