---
name: mmoframework
description: Write, review, or debug PHP code using MMOFramework (Marc Mouret's framework, namespace MMO\, package mmo.core) — its entities/repositories/collections/queries, documents/printers, HTTP server/routing/controllers, schemas, event system, and singleton/DI conventions. Use whenever a file starts `namespace MMO\` or `use MMO\core\...`, a class extends `aModule`/`aController`/`aEntity`/`Entity`, or the user mentions MMOFramework, mmo.core, mmo.json, or one of its classes (Application, Configuration, Route, Repository, Collection, Query, Entity, Document, Schema, etc.) by name — even if they don't say "MMOFramework" explicitly. Push hard to consult this whenever editing a repo that has a `bootstrap.php`/`mmo.json` at its root or a `src/` tree matching this framework's layout.
---
# MMOFramework
A PHP 8.3+ framework (namespace `MMO\`, root package `mmo.core`) built around: a DI/config container with
lazy singletons, an entity/repository/collection/query data layer, a document+printer rendering pipeline, a
PSR-7-flavored HTTP/CLI server layer with attribute-driven routing, and a plain-function event system. This
skill distills what running-code verification across the *entire* framework source (every file, ~40k lines)
established about how it actually behaves — not just what the docblocks claim.
## Before you write anything
1. **Find `bootstrap.php`** (or wherever the target project requires it) and confirm it's actually been
`require`d before any framework code runs, and that it `include`s every class file. No autoloader exists until
`\MMO\app()` has run (`Application` registers it); after that, a class whose namespace is registered as a path
(by its library's bootstrap; core's `{MMO\core}` by `Application` itself) is autoloaded with a warning, and
`documents/xlsx/*` is loaded lazily (see [conventions.md](https://mcp.mmoframework.fr/search/read?uri=mmoframework-skill%3A%2F%2Fmmo.core%2Fmmoframework%2Freferences%2Fconventions.md#lazy-xlsx-loading)).
2. **Identify the layer** you're working in (data model? rendering? HTTP/routing? validation?) and load the
matching reference file below — don't try to hold the whole framework in your head at once.
3. **Check [gotchas.md](https://mcp.mmoframework.fr/search/read?uri=mmoframework-skill%3A%2F%2Fmmo.core%2Fmmoframework%2Freferences%2Fgotchas.md) first** if something behaves unexpectedly — many of its entries
are things that *look* like reasonable framework behavior but were confirmed, via real execution, to be
bugs or non-obvious traps. If you're about to work around something weird, it's worth 30 seconds to check
whether it's already a documented, known issue (with a fix, or a reason it can't be fixed yet).
## The example to copy
`libs/core/demo` is a small, complete application written the way the framework expects — an entity with
validation, a JSON repository, a JSON API (`aDataController`), an html page from a template (`aController`), and an
event answered by a listening module. Served at `http://localhost/MMO/master/libs/core/`, tested by
`tests/demo/TestDemo.php`. When in doubt about how to write something, read its files first.
To start a new class, generate it rather than writing it from scratch: `php tools/mmo.php make entity|controller|test
<class> --lib=<lib>` (from the hub) writes the file, its bootstrap include and a first test. The class is relative to
the library's namespace, and the flags fill it in:
```sh
php tools/mmo.php make entity entities/Person --lib=core --prop=name,email,age:int --unique=email
php tools/mmo.php make controller controllers/PeopleController --lib=core --route="GET|people.json,POST|people.json"
```
Without a checkout, the MCP server's `generate_entity_stub`, `generate_controller_stub` and `generate_test_stub`
tools return the same code.
**A whole new project or library**, rather than a class: `php tools/mmo.php project ../acme.portal` scaffolds an
application, `php tools/mmo.php new mmo.foo` scaffolds a library and mounts it as a submodule (both take
`--dry-run`). See NewProject.md, NewLibrary.md and
Testing.md for what they write and why — including the two traps that bite hardest:
nothing autoloads on its own (every class file is `require`d by `src/bootstrap.php`), and a configuration path
into your own code is `{YourNamespace}/...`, never `{root}/src/...`.
## The public MCP server
`https://mcp.mmoframework.fr/mcp.json` (streamable HTTP, `POST` only, no authentication) serves the documentation
and the API of every released mmo.* library, from any machine — no checkout of the framework needed. Connect it
with `claude mcp add --transport http mmoframework https://mcp.mmoframework.fr/mcp.json`. When it is connected,
**ask it before guessing**: `search_documentation` to find a class, function or guide by name, `documentation_index`
to see everything documented, `describe_class`/`describe_function` for a signature, and its
`mmoframework-skill://` resources for this skill. It describes the released libraries, not the application you are
working on: for that application's own routes, entities and classes, use its own MCP server (`POST <its
url>/mcp.json`, see "Testing HTML output") when it has one.
## Reference map
| Reference | Load it when you're... |
|---|---|
| [conventions.md](https://mcp.mmoframework.fr/search/read?uri=mmoframework-skill%3A%2F%2Fmmo.core%2Fmmoframework%2Freferences%2Fconventions.md) | touching *anything* — the singleton/`Shared()` pattern, `Configuration`/DI, `#[Serialize]`, namespace layout, top-level `\MMO\*` helpers, exceptions |
| [entities.md](https://mcp.mmoframework.fr/search/read?uri=mmoframework-skill%3A%2F%2Fmmo.core%2Fmmoframework%2Freferences%2Fentities.md) | modeling data: `Entity`, `Collection`, `Repository`/`JsonRepository`, `Query`, filters, `#[ArrayProperty]`, `cStatus` |
| [documents-and-printers.md](https://mcp.mmoframework.fr/search/read?uri=mmoframework-skill%3A%2F%2Fmmo.core%2Fmmoframework%2Freferences%2Fdocuments-and-printers.md) | rendering output: `Document`/`HtmlDocument`/`JsonDocument`/`XmlDocument`, templates (`import()`), xlsx spreadsheets, printers |
| [server-http.md](https://mcp.mmoframework.fr/search/read?uri=mmoframework-skill%3A%2F%2Fmmo.core%2Fmmoframework%2Freferences%2Fserver-http.md) | HTTP/CLI plumbing: `Request`/`Response`/`Route`/`Stream`, `Curl`, `Mail`, `SSH`, `Websocket`, `Cron` |
| [controllers-and-routing.md](https://mcp.mmoframework.fr/search/read?uri=mmoframework-skill%3A%2F%2Fmmo.core%2Fmmoframework%2Freferences%2Fcontrollers-and-routing.md) | writing a controller: `aController`/`aDataController`/`aShellController`, `#[APIRoute]`/`#[Command]`, wiring routes into `Application` |
| [schemas-and-events.md](https://mcp.mmoframework.fr/search/read?uri=mmoframework-skill%3A%2F%2Fmmo.core%2Fmmoframework%2Freferences%2Fschemas-and-events.md) | validation (`Schema`/`Validator`/`Coercer`) or the event system (`Event`/`AddListener`/`::Send()`) |
| [gotchas.md](https://mcp.mmoframework.fr/search/read?uri=mmoframework-skill%3A%2F%2Fmmo.core%2Fmmoframework%2Freferences%2Fgotchas.md) | debugging something that seems wrong, or before relying on any non-obvious behavior |
| NewProject.md | starting an application from nothing: the manifest, the root `bootstrap.php` and its mode, the config layering, `www/index.php` |
| NewLibrary.md | creating a `mmo.*` library: `tools/mmo.php new`, the name rule, the manifest, `src/bootstrap.php` |
| Testing.md | setting up or running tests: `phpunit.xml`, `tests/bootstrap.php`, the `TestThing` convention |
## The things that matter most
These come up in almost every task, so they're worth internalizing rather than looking up each time:
1. **Never `new` a singleton class.** Anything using `tSingleton` (most `aModule` subclasses — `Application`,
`PackageManager`, `Cron`, `I18n`, every controller) is accessed via `ClassName::Shared()`, never
`new ClassName()` — so is `Configuration`, which has a `Shared()` of its own. See
[conventions.md](https://mcp.mmoframework.fr/search/read?uri=mmoframework-skill%3A%2F%2Fmmo.core%2Fmmoframework%2Freferences%2Fconventions.md#singletons).
2. **A route handler returning a plain array needs a response already set on the route.** Extend
`aDataController` (auto-sets a JSON response in its own `onRoute()`) if a plain array return is all you
need; extend plain `aController` and either return an `iResponse` directly or implement your own `onRoute()`
otherwise — a bare `aController` subclass returning an array crashes with "route has no response set."
Verified directly; see [controllers-and-routing.md](https://mcp.mmoframework.fr/search/read?uri=mmoframework-skill%3A%2F%2Fmmo.core%2Fmmoframework%2Freferences%2Fcontrollers-and-routing.md#base-classes--pick-the-right-one).
3. **`import()` does a literal filename lookup (no extension magic) and does NOT `extract()` `$vars`.** Pass
the real on-disk filename verbatim, extension included when the file has
one (`$document->import('welcome.phtml', $vars)`, not `'welcome'`) — and read values inside the template as
`$_VARS['key']`, never a bare `$key`. See
[documents-and-printers.md](https://mcp.mmoframework.fr/search/read?uri=mmoframework-skill%3A%2F%2Fmmo.core%2Fmmoframework%2Freferences%2Fdocuments-and-printers.md#templates).
4. **`HtmlPrinter` prints strings as raw markup unless told to escape.** That is on purpose (HTML documents
embed markup through it), but it means user-supplied text printed into an `HtmlDocument` is an XSS. In a
template, `htmlspecialchars($value, ENT_QUOTES)` what you print (as the demo's templates do); through the
printer, pass `new PrintOptions(escape: true)` (`$this->renderValue($value, new PrintOptions(escape: true))`),
or give the whole document `new DocumentOptions(print: new PrintOptions(escape: true))`. Pass options typed
like this rather than as an array: a misspelled array key (`['escpae' => true]`) is silently ignored, a
misspelled argument is an error. See
[gotchas.md](https://mcp.mmoframework.fr/search/read?uri=mmoframework-skill%3A%2F%2Fmmo.core%2Fmmoframework%2Freferences%2Fgotchas.md#htmlprinter-does-not-escape-strings).
5. **Parenthesize `??` next to `===`/`&&`/`||`.** `??` binds *looser* than all of those, and this exact
footgun caused several real, shipped bugs (`$a === $b ?? $c` parses as `($a === $b) ?? $c`, which — since a
`===` result is never `null` — silently makes the `?? $c` fallback permanently dead code). See
[gotchas.md](https://mcp.mmoframework.fr/search/read?uri=mmoframework-skill%3A%2F%2Fmmo.core%2Fmmoframework%2Freferences%2Fgotchas.md#-vs--precedence).
6. **Check input with `Entity::CheckData($input, $errors)` before creating or changing an entity.** An entity
checks values as they are set in dev mode only, and a value it cannot convert is simply not set. See
[entities.md](https://mcp.mmoframework.fr/search/read?uri=mmoframework-skill%3A%2F%2Fmmo.core%2Fmmoframework%2Freferences%2Fentities.md#validating).
## Verifying your work
"Done" means `verify` passes. From the hub's root (the folder holding `libs/`, `tools/` and `phpstan.neon.dist`):
```bash
php tools/mmo.php verify --lib=core
```
It lints every changed php file, runs phpstan on them (only errors that are *not* in `phpstan-baseline.neon`
are reported, so anything it prints is yours), and runs the library's phpunit suite. `--path=a.php,b.php`
checks given files, `--all` every library, `--no-tests` skips phpunit. In Claude Code, a hook already runs the
lint + phpstan half after every edit of a php file and hands back what it finds.
If you're not sure whether something behaves the way you think, **write a tiny real script and run it** —
that discipline (verify via execution, never via reasoning alone) is how every fact in this skill was
established, and it catches real bugs that reading the code won't. This framework is unusually easy to test in
isolation: `require 'bootstrap.php';` then use the class directly, no server or database needed for most of it
(`JsonRepository` persists to a plain JSON file).
### Testing HTML output
Every project under `~/dev/src` is served by apache on localhost, at its path below `~/dev/src`:
`~/dev/src/<project>` is `http://localhost/<project>/` — e.g. the project `mcp.mmoframework` is
`http://localhost/mcp.mmoframework/`, and its MCP server `POST http://localhost/mcp.mmoframework/mcp.json` (a
GET answers 405: no SSE stream). A project's root `.htaccess` routes every url to `www/index.php`. The hub is served
at `http://localhost/MMO/master/`, and every library's demo app at by apache at `http://localhost/MMO/master/`, and every library's demo app at
`http://localhost/MMO/master/libs/<library>` (which maps to `libs/<library>/demo/www`) — e.g.
`http://localhost/MMO/master/libs/client`. Use that (`curl`, or a browser) rather than starting a server of your
own. The MCP server (`POST http://localhost/MMO/master/mcp.json`, connected in dev mode) describes the running
application's classes, routes and entities — see `docs/MCP.md`.
A project using mmo.oauth answers every page with its login form. Don't type a password into it: run
`php admin.php agent-login --user=<username> --url=http://localhost/<project> -m` from the project's folder and
open the `data.url` it prints. `GET pages.json` then lists every page to visit. The `mmo-agent-login` skill
(`libs/oauth/.claude/skills/mmo-agent-login/`) has the details.