Core

conventions

  • mmo.core
  • agent skill
  • mmoframework-skill://mmo.core/mmoframework/references/conventions.md
# 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").