A concise demonstration of PHP's module feature (RFC: first-class domain encapsulation). Built as a configuration-loading library so the public-API-vs-internals boundary is immediately intuitive.
| Feature | How It's Shown |
|---|---|
module PHPModules\Demo\Config { ... } block |
src/Config.php — claims every member, declares inline enum + const |
Inline enum in module block |
SupportedFormats — cases Json, Yaml, Env |
internal const (module-level) |
PHPModules\Demo\Config::DEFAULT_CACHE_TTL — hidden from outside |
| Forward declarations (claims) | All 10 classes claimed in the definition block |
Split-file membership (module X;) |
Every class in src/Config/ is a separate membership file |
public class |
Loader, Repository, FileSource, EnvSource |
internal class |
JsonParser, YamlParser, EnvParser, Merger, SchemaValidator |
internal method |
Loader::resolveParser(), ConfigValue::freeze() |
internal __construct (factory pattern) |
Repository — only the module may instantiate it |
module:: self-reference |
Loader::load() resolves internal collaborators via module::Merger(), module::SchemaValidator() |
| Boundary enforcement | Outside code cannot: new Repository(...), new Merger(), call $loader->resolveParser() |
Module PHPModules\Demo\Config maps to src/Config.php; member classes under src/Config/ match PSR-4 conventions.
project-deepseek/
├── README.md
├── entry.php # Run this to see the demo
└── src/
├── Config.php # module PHPModules\Demo\Config { ... }
└── Config/
├── Loader.php # public — orchestrates loading
├── Repository.php # public — parsed config store
├── ConfigValue.php # public — typed value wrapper
├── FileSource.php # public — reads config files
├── EnvSource.php # public — reads environment vars
├── JsonParser.php # internal — parses JSON
├── YamlParser.php # internal — parses YAML
├── EnvParser.php # internal — parses KEY=VALUE
├── Merger.php # internal — deep-merges arrays
└── SchemaValidator.php # internal — validates required keys
/path/to/php-modules-sapi/php entry.phpThe script loads sample config (from a temp file + env vars), dumps the result, then probes the module boundary three ways to show enforcement errors.
| Class | Role |
|---|---|
Loader |
Add sources (file or env), call load() to get a Repository |
Repository |
Immutable key-value store (obtained from Loader::load()) |
ConfigValue |
Value object wrapping a single config key |
FileSource |
Reads a file, reports its format (json, yaml) |
EnvSource |
Reads $_ENV vars matching a prefix, format is env |
| Class | Role |
|---|---|
JsonParser |
json_decode with error handling |
YamlParser |
Minimal YAML line parser |
EnvParser |
Parses KEY=VALUE lines |
Merger |
Recursive array merge |
SchemaValidator |
Checks required keys and value types |
| Name | Visibility | Kind |
|---|---|---|
SupportedFormats |
public | enum with cases Json, Yaml, Env |
DEFAULT_CACHE_TTL |
internal | const = 300 |
Why config loading? Every developer knows what config is. The boundary practically writes itself: users interact with Loader and Repository; parsers, merger, and validator are implementation details that belong inside the module. Trying to reach them from outside produces a clear runtime Error — the language itself enforces what was previously only a convention.