# Core Conventions
Read this first for any MMOFramework task — these patterns apply across every layer.
## Namespace layout
Everything lives under `MMO\`. Two kinds of things:
- **Plain functions**, grouped by domain: `\MMO\core\arrays\*`, `\MMO\core\strings\*`, `\MMO\core\dates\*`,
`\MMO\core\files\*`, `\MMO\core\paths\*`, `\MMO\core\uris\*`, `\MMO\core\server\*`, `\MMO\core\shell\*`,
`\MMO\core\objects\*`, `\MMO\core\versions\*`, `\MMO\core\schemas\*`, `\MMO\core\packages\*`,
`\MMO\core\debug\*`, `\MMO\core\colors\*`. These are just functions, not classes — `\MMO\core\files\get($path)`,
not `Files::get()`.
- **Classes**, under `MMO\core\{classes,entities,server,documents,printers,schemas,helpers,events,
exceptions,attributes,interfaces,controllers}\...`, plus a handful of top-level ones directly under `MMO\core\`
(`Application`, `Configuration`, `Cron`, `I18n`, `PackageManager`, `JsonParser`, `XmlParser`, `ImportSocket`).
A small set of **top-level `\MMO\*` helper functions** (in `functions.php`) are the ergonomic entry points most
application code actually calls:
| Function | Does |
|---|---|
| `\MMO\app()` | `Application::Shared()` |
| `\MMO\config(...$configs)` | load config into the shared `Configuration` |
| `\MMO\get($path, $resolve = true)` | read a config value by `/`-path (@see [Configuration](#configuration-di-container)) |
| `\MMO\set($path, $value)` | write a config value by path |
| `\MMO\create($origin, $class, $args = [])` | dispatch a `Create` event (configurable object construction) |
| `\MMO\fetch($origin, $href, $options = [])` | dispatch a `Fetch` event — resolve a reference/URL/entity by id |
| `\MMO\commit($origin, $options = [])` | dispatch a `Commit` event — persist every entity currently marked changed |
| `\MMO\getCommit($origin, $options = [])` | build the `Commit` without dispatching it (inspect before committing) |
| `\MMO\connect($routes)` | register controller routes on the application |
| `\MMO\filename($uri, $base = '{root}')`, `\MMO\url($uri, $base = '{siteurl}')` | resolve a path-aliased uri (@see [conventions.md](#path-aliases)) |
| `\MMO\dump(...$values)`, `\MMO\idump($value)` | debug-print via the current printer |
| `\MMO\info()`/`\MMO\debug()`/`\MMO\warning()`/`\MMO\error()`/`\MMO\critical()`/`\MMO\notice()` | emit a log at that level: `(origin, message, context = [])` |
| `\MMO\log()` | emit a log at a given level: `(origin, level, message, context = [])` |
`inc()`/`isIncluded()` load framework packages by name+version (see `PackageManager`) — you'll rarely call
these directly outside of bootstrap-level code.
## Singletons
Anything using the `tSingleton` trait — which includes most `aModule` subclasses (`Application`,
`PackageManager`, `Cron`, `I18n`, every `aController` subclass) — is a **per-request singleton**, retrieved with
`ClassName::Shared()`. Never `new ClassName()` for these; the constructor is typically `protected`.
`Configuration` does not use the trait but has a `Shared()` of its own: never `new` it either.
```php
$app = Application::Shared();
$cron = Cron::Shared(); // NOT new Cron()
MyController::Shared()->doThing();
```
`Shared()` lazily builds the instance from whatever config was loaded for that class name (see below), the
first time it's called, and caches it in `Configuration`. `ClassName::Unshare()` discards the cached instance
(by default, first serializing it back into config so the *next* `Shared()` call picks up where it left off —
pass `false` to just discard it).
## Configuration (DI container)
`Configuration::Shared()` is a single nested array of `class-or-path => settings-or-instance`, populated via
`\MMO\config([...])` (or `Configuration::Shared()->load([...])`), and read via `\MMO\get('Some/Path')` or
`Configuration::Shared()->get(...)`.
```php
\MMO\config([
MyRepositoryController::class => [
'repositories' => ['main' => ['$' => '#repo']],
],
'repo' => [
'$' => \MMO\core\entities\JsonRepository::class,
'filenames' => ['items' => __DIR__ . '/data/items.json'],
'collections' => ['items' => MyEntity::class],
],
]);
$repo = \MMO\get('repo'); // resolves the '$' => Class descriptor into a real instance
$ctrl = MyRepositoryController::Shared(); // its 'repositories' config was injected automatically
```
Key points:
- A config value shaped `['$' => ClassName, ...otherArgs]` is a **lazy object descriptor** — `Configuration::get()`
unserializes it into a real instance the first time it's read, and caches the instance in place of the
descriptor (so reading it twice returns the same object).
- Its other keys are the class's **property names**, set without calling the constructor. A key that is not one
is an error when the object is built: `unknown property [filenmaes] of [MMO\core\entities\JsonRepository]. did
you mean [filenames]?`.
- `#[Serialize(...)]` on a class property controls how that property round-trips through this system — e.g.
`#[Serialize(unserialize: 'setFoo')]` means "when this class is built from config, call `setFoo($value)`
instead of just assigning the property"; `#[Serialize(false)]` means "never serialize/restore this property
at all" (used for caches, live resources, computed state).
- Application config's own key (`Application::class`) is always injected **last**, regardless of what order you
pass configs in — so it can override anything else. (This ordering was a real, fixed bug — see the NOTE in
`Configuration::load()`.)
## Path aliases
`Application` keeps a map of `{name} => path` aliases (`{root}`, `{basepath}`, `{baseurl}`, `{siteurl}`,
`{tmp}`, plus whatever you register via `Application::Shared()->setPath('name', 'value')`). Values can
reference each other (`{root}/subdir`) and get expanded recursively by `resolvePath()`.
`\MMO\filename('{root}/data/file.json')` and `\MMO\url('{siteurl}/api.json')` resolve these into real absolute
paths/URLs — use them instead of hardcoding paths, so code stays portable across environments.
## Exceptions
All framework exceptions live in `MMO\core\exceptions\` and implement `iException` via the `tException` trait.
The hierarchy roots at a few bases you should throw, not invent new top-level exception types for casually:
- `eInvalid` (extends `\InvalidArgumentException`) — bad input/argument. Has more specific subtypes:
`eInvalidConfiguration`, `eInvalidCoercion`, `eInvalidVersion`, `eJson` (malformed JSON).
- `eRuntime` — generic "something went wrong at runtime" (most common catch-all).
- `eNotFound` / `eItemNotFound` — a lookup failed.
- `eLogic`, `eReadonly`, `eTimeout`, `eServer`, `e404`, `eDeprecated` — self-explanatory.
Construction pattern (used everywhere): `new eRuntime($this, 'message with {placeholder}.', ['placeholder' => $value])`
— first arg is the "origin" (usually `$this`, used for logging/debug context), second is a message with
`{curly}` placeholders, third is the context array filled in via `\MMO\core\strings\printf()`-style substitution.
`eServer` and `e404` take the request (or the `Route`) second, before the message:
`new e404($this, $route, 'no person [{id}].', ['id' => $id])`.
An exception a route lets out becomes its error response (`Application::createErrorResponse()`): the status is the
exception's code when that is an HTTP status, 500 otherwise. `e404`'s code is 404, the others' is 0 unless given as
the fifth argument (`new eInvalid($this, 'bad [{key}].', ['key' => $key], null, 400)`). A
`PDOException`'s code is a string: see [gotchas.md](https://mcp.mmoframework.fr/search/read?uri=mmoframework-skill%3A%2F%2Fmmo.core%2Fmmoframework%2Freferences%2Fgotchas.md#an-exceptions-code-is-not-always-an-int).
## Lazy xlsx loading
`documents/xlsx/*` (spreadsheet reading/writing) is the one part of the framework not eagerly included by
`bootstrap.php` — it's registered via `Autoload::OnNamespace('MMO\\core\\documents\\xlsx', ...)` and only
actually `require`s those files the first time something in that namespace is touched. If you're writing a
throwaway test script that needs xlsx classes and bootstrap.php alone doesn't seem to load them, this is why —
the autoloader that sends that event is registered by `Application`, so call `\MMO\app()` first: referencing the
class (e.g. `new \MMO\core\documents\xlsx\Document()`) then triggers the lazy load (before it: `Class not found`).
The files are included once: a class of that namespace still unknown afterwards does not exist (until 2026-09,
looking one up -- as reading a docblock that names an imported class does -- included them all again: a fatal
"Cannot redeclare class").