Methods36
Refuse a write one of whose values holds an item that could not be converted (an unknown uid in a list): the entity kept that property's previous value -- a new list item's parent, an empty list -- and the write answered 200 as if all had been saved (my.oviepro #228). A value that cannot be set as a whole (a draft's empty date) is still taken, and said in the entity's errors.
protected function checkItems(MMO\core\interfaces\iEntity $entity, array $data): void| $entity | iEntity | just loaded with $data |
| $data | array | what the client sent for it |
| returns | void | |
| throws | | (422) naming each item from the entity, and why: nothing is changed |
PUT `{"id": "12", "suiviDossier": {"arrets": [{"uid": "unknown"}]}}`: 422, `[suiviDossier/arrets/0] unknown entity identifier [unknown].`
Refuse to create an entity from an entry none of whose keys is a property of its class: loadArray() drops an unknown key (logged only), so a body in another format -- wrapped ({"data": {...}}), another entity's -- created an empty entity and answered 200 (my.oviepro #334). An entry with no key at all ({"id": null}, a status only, [{}]) still creates an entity of default values; one holding a known key beside unknown ones is still taken.
protected function checkKnownProperties(MMO\core\interfaces\iCollection $collection, array $entries): void| $collection | iCollection | |
| $entries | array | |
| returns | void | |
| throws | | (422) naming the unknown keys: nothing is changed |
POST entreprises.json `{"data": {"label": "ACME"}}`: 422 « Nothing to create in [entreprises]: [data] is no property of its entries. »
Refuse to create an entity that has the values of an existing one on one of its class's @unique groups (its own, its parent classes' and their traits': classUniqueGroups): one created by the same request included, an archived one too.
protected function checkUnique(
MMO\core\server\Route $route,
MMO\core\interfaces\iCollection $collection,
array $data
): voidNOTE: until 2026-10 `@unique` only found the entity a POST without `id` updates: one with `"id": null` created a
second account of the same username (my.oviepro #268)| $route | Route | |
| $collection | iCollection | |
| $data | array | |
| returns | void | |
| throws | | (409 Conflict) naming the fields of the group: nothing is changed |
POST googleUsers.json `{"id": null, "username": "ada@x.com"}` when ada@x.com exists: 409 « Une fiche avec
cette valeur de « Email » existe déjà. »
Refuse an update that gives an entity the values another entity of its collection has on one of its class's @unique groups (an archived one included): a list sent again without id, found by one group (a matricule) and carrying another's value of a second one (an address), or a PUT changing a unique value to one already used.
protected function checkUniqueChanges(
MMO\core\server\Route $route,
MMO\core\interfaces\iCollection $collection,
MMO\core\interfaces\iEntity $entity
): voidOnly the groups the update changed are checked, and a group with an empty value identifies nothing.
NOTE: until 2026-10 only a creation was checked ([`checkUnique`](https://mmoframework.fr/en/documentation/mmo.core/php/controllers/RepositoryController.md#repositorycontroller-checkunique)): an update gave two accounts the same username,
or the matricule of another account to the one its address named (my.oviepro #350)POST googleUsers.json `{"eid": "1", "username": "bob@x.com"}` when bob@x.com is another's: 409
protected static function ClassFiles(string $class): array| $class | string | |
| returns | array | the files $class is written in: its own, its parents', its interfaces' and its traits' |
static function CollectionUrl(
string $collection,
string $repository = 'main',
string $extension = 'json',
MMO\core\helpers\FilterGroup|array|string $query = ''
): stringCalled once when routes are registered: adds the actual list of configured repository/collection names as an enum constraint onto every route's repository/collection API parameter info (so generated API docs/validation know the real allowed values, instead of just 'string').
static function ConnectRoutes(array &$routes, array $config): void| $routes | array | |
| $config | array | unused directly -- get is used instead to read this controller's own configured repositories. |
| returns | void | |
Create an entity of $collection with $data, its status set through its setStatus() -- the one asked, or its class's default: born a draft, it goes through the guards a draft leaving the draft goes through.
protected function createEntity(
MMO\core\interfaces\iCollection $collection,
array $data,
?MMO\core\entities\cStatus $status
): MMO\core\interfaces\iEntityA status other than the default stays a change of the entity created: the commit's listeners check the right to set
it (mmo.oauth's AccessController: `data#final`, `data#validated`), as for an entity changed.
NOTE: until 2026-10 the status was loaded with the data, never checked: a declaration created by the api was
active, incomplete (#142), and a client without the right to validate created an entity validated.$this->createEntity($collection, ['numero' => '12'], cStatus::ACTIVE);
function getCollection(string $name, ?string $repository = null): MMO\core\interfaces\iCollection| $name | string | |
| $repository = null | null | string | if given, only look in that repository; if null, search every registered one. |
| returns | iCollection | |
| throws | | if no matching collection is found. |
Where the entities of $class are listed from: the collection of a repository served whose class it is, or, for a class no collection holds (an abstract one), the one collection holding a class of its kind. A client lists the entities a reference can point to from it (mmo.client's <mmo-search>).
function getCollectionOf(string $class): ?arrayNOTE: until 2026-10 the schemas did not tell it, and mmo.client guessed it from the class's name (`Contrat` in
`contrats`), which an abstract class (`aGroupe`) or a collection named otherwise defeated (my.oviepro #279)| $class | string | |
| returns | null | array | string, collection: string} null when no collection, or several, may hold it |
$controller->getCollectionOf(Contrat::class); // ['repository' => 'main', 'collection' => 'contrats']
What a PATCH's entry asks, checked before anything changes: its status, or the items it pushes onto list properties.
protected function getPatchValues(
MMO\core\server\Route $route,
MMO\core\interfaces\iEntity $entity,
array $data
): arrayEach key other than `id` and `status` names a list property of the entity, or of an entity inside it
(`path/to/list`, `commentaires` or `suiviDossier/arrets`); its value is one item, or a list of items.| $route | Route | |
| $entity | iEntity | |
| $data | array | |
| returns | array | ?cStatus, pushes: list<array{entity: iEntity, key: string, path: string, items: list<mixed>}>} the status asked (the entry's other keys are then ignored, as they always were), or what to push where |
| throws | | (400 Bad Request) for an unknown status, a key whose path leads to no entity, or that names no list property: a PATCH only pushes items, a PUT sets a value |
$this->getPatchValues($route, $entity, ['id' => '12', 'tags' => 'x']); // ['status' => null, 'pushes' => [[$entity, 'tags', 'tags', ['x']]]]
function getQuery(): ?MMO\core\entities\Queryfunction getRepositories(): array| returns | array | [iRepository] |
function getRepository(string $name): MMO\core\interfaces\iRepositoryfunction getRepositoryName(MMO\core\interfaces\iRepository $repo): ?stringfunction getSchemaClasses(): array| returns | array | the entity class of every collection of every repository served: what routeSchemas describes |
The $defs of the schemas document (routeSchemas): the schema of every class served, and of every class they use, each class held by a collection of a repository served given its $collection (getCollectionOf).
function getSchemaDefs(): array| returns | array | Schema|array<string, mixed>> |
$controller->getSchemaDefs()['App\User']['$collection']; // ['repository' => 'main', 'collection' => 'users']
protected function getUrlId(MMO\core\server\Route $route): ?string| $route | Route | |
| returns | null | string | the entity the url names (?id=), or null when it names none |
| throws | | (400 Bad Request) for an id that is not a single value |
$this->getUrlId($route); // '12' for `PUT data/main/users.json?id=12`
The entities a write's entries name, all found before anything changes: one id that names no entity refuses the whole request, as an entry naming none clearly does (getWriteEntries).
protected function getWriteEntities(
MMO\core\server\Route $route,
MMO\core\interfaces\iCollection $collection,
array $entries
): arrayNOTE: until 2026-10 an unknown id was skipped and logged, the request answering 200 with the other entries written
(#141)| $route | Route | |
| $collection | iCollection | |
| $entries | array | |
| returns | array | the entity of each entry, at its index; null for an entry without an id (a POST's creation) |
| throws | | naming every unknown id: nothing is changed |
$this->getWriteEntities($route, $collection, [['id' => '12']]); // [the entity 12], or a 404
The entries of a write's body (PUT, POST, PATCH, DELETE), each naming the entity it is for without ambiguity, all checked before anything changes: one that does not refuses the whole request.
protected function getWriteEntries(MMO\core\server\Route $route, bool $needsId = true): array- `?id=` in the url names one entity: the body then holds that one only, its `id` left out (it is the url's) or
the same. A DELETE or a PATCH... with no body is for that entity;
- otherwise every entry of a PUT, a PATCH or a DELETE gives its `id`; a POST's entry without one creates an
entity (or finds one with the same data: [`routePost`](https://mmoframework.fr/en/documentation/mmo.core/php/controllers/RepositoryController.md#repositorycontroller-routepost)).
NOTE: until 2026-10 the url's `?id=` was ignored and an entry without an `id` skipped, the request answering 200: a
client that had taken the wrong entity's data for its own (a listing's first, its `?id=` being ignored too) changed
that one, believing it was naming the entity of its url (#141)| $route | Route | |
| $needsId = true | bool | false for a POST |
| returns | array | mixed>> the entries, an entry named by the url holding its id |
| throws | | (400 Bad Request) for a body that is not a list of objects, an entry without an id (when $needsId), an id that is not a value, a body of several entries or naming another entity than the url |
$this->getWriteEntries($route); // [['id' => '12', 'name' => 'Ada']] for `PUT users.json?id=12` {"name": "Ada"}
function hasCollection(
MMO\core\interfaces\iCollection|string $collection,
?string $repository = null
): boolTell if a query of $entity's collection, dispatched as a listing's (routeGet), keeps it: its listeners decide (mmo.oauth's AccessController, an application's filter by client)
protected function isListed(mixed $entity): bool| $entity | mixed | |
| returns | bool | false for a value that is not an entity of a collection |
$this->isListed(\MMO\fetch($this, 'file://users.json#1')); // false if a listing of users would leave it out
function onRoute(MMO\core\server\Route $route): bool- See
iRoutable::onRoute -- prepares the response for this route's extension before the route's own handler runs: JSON (default), CSV, or Xlsx (optionally from a template, $xlsxTplFilename), each with a Content-Disposition: attachment filename derived from today's date and the {repository}/{collection} URL.
Delete entities by id -- archives an active entity (iEntity::archive), or permanently removes an already-archived one.
#[MMO\core\attributes\APIRoute('DELETE|data/{repository}/{collection}.{extension}', ['parameters' => […10 items]], mediaTypes: ['json', 'csv'])]
function routeDelete(MMO\core\server\Route $route): array| $route | Route | the request body: a single {id: ...} object, or a list of them; or none, the url naming the entity (?id=): getWriteEntries. |
| returns | array | every entity archived/removed. |
| throws | | if repository/collection doesn't resolve. |
| throws | | if the collection is readonly. |
| throws | | (400 Bad Request) when an entry does not name its entity (getWriteEntries): nothing is changed. |
| throws | | for an id that names no entity (getWriteEntities): nothing is changed. |
Fetch a single entity by its global uid (fetch), regardless of which repository/collection it's in.
#[MMO\core\attributes\APIRoute('GET|data.json?uid={uid}', ['parameters' => […10 items]], mediaTypes: ['json'])]
function routeEntity(MMO\core\server\Route $route): arrayWhat a listing would not show is refused ([`isListed`](https://mmoframework.fr/en/documentation/mmo.core/php/controllers/RepositoryController.md#repositorycontroller-islisted)): the entity the uid names, and one of another collection
that its path leads to (`...#12/author`; an embedded one, `...#12/address/0`, is its entity's).
NOTE: until 2026-09 nothing was, \MMO\fetch() dispatching no query: `data.json?uid=file://users.json#1` gave any
client the user that `data/main/users.json` refused it.| $route | Route | |
| returns | array | [the entity]. |
| throws | | if no entity matches, or a listing would leave it out. |
GET data.json?uid=file://users.json#1 // [the user], if a listing of users would show it
List entities from one collection: pagination, sort, field selection, filters, and text/property search (the class docblock's "Supported query" section), plus first/prev/next/last pagination links (NOTE: see the caveat on 'data.total'/'page.total'/'last' below -- task_03489439).
#[MMO\core\attributes\APIRoute('GET|data/{repository}/{collection}.{extension}', ['parameters' => […10 items]], mediaTypes: ['json', 'csv', 'xlsx'])]
function routeGet(MMO\core\server\Route $route): array| $route | Route | |
| returns | array | the matching entities for this page. |
| throws | | if repository or collection doesn't resolve. |
Partially update entities by id -- either sets a new cStatus (via a 'status' key), or pushes one or more values onto array properties (any other key, optionally a path/to/nested/prop for a nested entity, via iArrayProperties::setArrayPropertyOffset).
#[MMO\core\attributes\APIRoute('PATCH|data/{repository}/{collection}.{extension}', ['parameters' => […3 items]], mediaTypes: ['json', 'csv'])]
function routePatch(MMO\core\server\Route $route): array| $route | Route | the request body: a single {id, ...} object, or a list of them; the url can name the one entity of the body (?id=): getWriteEntries. |
| returns | array | every entity actually changed (an archived/final target is skipped and logged, not thrown). |
| throws | | if repository/collection doesn't resolve. |
| throws | | if the collection is readonly. |
| throws | | (400 Bad Request) when an entry does not name its entity (getWriteEntries), asks an unknown status, or holds a key that is no list property of the entity at its path (getPatchValues): nothing is changed. |
| throws | | (422) what the entity's setStatus() refuses, an item the list does not take: nothing is changed. |
| throws | | for an id that names no entity (getWriteEntities): nothing is changed. NOTE: until 2026-10 the route required an id in the body: PATCH ...json?id=12 matched no route and answered the html 404 page; a key naming a property that is not a list was a 500 (array_is_list() given a string); an unknown status, a path leading nowhere or an item refused were skipped and logged, the request answering 200 (my.oviepro #262) |
Insert or update entity with similar data (using groups or id)
#[MMO\core\attributes\APIRoute('POST|data/{repository}/{collection}.{extension}', ['parameters' => […10 items]], mediaTypes: ['json', 'csv'])]
function routePost(MMO\core\server\Route $route): array| $route | Route | the request body: a single entity object, or a list of them. Per entity: an 'id' key with a value updates that entity; an 'id' key present but explicitly null forces creation of a new entity; no 'id' key at all looks for (or creates) an entity with matching content (Collection::getOrCreateEntity). |
| returns | array | every entity actually created or changed (an archived/final entity being updated is skipped and logged, not thrown; an unchanged existing entity is silently omitted). |
| throws | | if repository/collection doesn't resolve. |
| throws | | if the collection is readonly. The url can name the one entity of the body (?id=, getWriteEntries). A status goes through the entity's setStatus() once the rest is loaded; an entity created is born a draft and given the status asked, or its class's default, the same way (createEntity). |
| throws | | if nothing ended up being inserted/updated. |
| throws | | (400 Bad Request) for an entry naming no entity clearly (getWriteEntries), or an unknown status: nothing is changed. |
| throws | | (422) what the entity's setStatus() refuses (a declaration created active but incomplete), or an entry to create none of whose keys is a property of the entity (checkKnownProperties): nothing is changed. |
| throws | | for an id that names no entity (getWriteEntities): nothing is changed. |
Asked with its version (?v=, the url SchemaUrl gives), the document is kept by the browser for a year: another version is another url. Without it, or with a version that is no longer the current one (a page loaded before a release), it is not kept.
#[MMO\core\attributes\APIRoute('GET|data/schemas.json', 'Get schemas for all the entities in the application', mediaTypes: ['json'])]
function routeSchemas(MMO\core\server\Route $route): array| $route | Route | |
| returns | array | a JSON Schema document ($id/$defs) covering every entity class served by every registered repository/collection. |
Update entity only by id
#[MMO\core\attributes\APIRoute('PUT|data/{repository}/{collection}.{extension}', ['parameters' => […10 items]], mediaTypes: ['json', 'csv'])]
function routeUpdate(MMO\core\server\Route $route): array| $route | Route | the request body: a single entity object, or a list of them; each MUST have an 'id', or the url names the one entity of the body (?id=): getWriteEntries. A status goes through the entity's setStatus(), once the rest is loaded. |
| returns | array | the entities that were actually changed (an archived/final entity, or no actual changes, is skipped and logged, not thrown). |
| throws | | if repository/collection doesn't resolve. |
| throws | | if the collection is readonly. |
| throws | | (400 Bad Request) when an entry does not name its entity (getWriteEntries), or for an unknown status: nothing is changed. |
| throws | | (422) what the entity's setStatus() refuses (an incomplete declaration validated): nothing is changed. |
| throws | | for an id that names no entity (getWriteEntities): nothing is changed. |
A fingerprint of the schemas document (routeSchemas): the hash of its content. Building it costs (20 to 30 ms for my.oviepro), so it is kept in a file of {tmp}, with the mtime of every file its classes are written in; it is built again when one of them changes, or when the packages' versions, the classes served (and their collections) or the translations of the current language are others (each its own file).
static function SchemasVersion(): stringOn the command line (a test, a script) it is kept for the process only: each run serves other classes or translations,
and every one left its file in `{tmp}` (my.oviepro #268).| returns | string | 16 hexadecimal characters |
RepositoryController::SchemasVersion(); // '3f2a9c0d81b7e645'
The url of the schemas document, versioned (?v=, SchemasVersion): the browser keeps it (routeSchemas) until the schemas change, and an entity changed, a release or another language make another url.
static function SchemaUrl(
MMO\core\interfaces\iEntity|MMO\core\interfaces\iCollection|null $entity = null
): stringNOTE: until 2026-10 the url was not versioned and the document (300 Ko for my.oviepro) not cachable: every page
loaded it again before its forms and lists could show their values (my.oviepro #147).RepositoryController::SchemaUrl($users); // http://localhost/app/data/schemas.json?v=3f2a...#$defs/App\User
Replace every registered repository (Serialize -- this is the injection target for the repositories config key, e.g. RepositoryController::class => ['repositories' => ['main' => ...]]).
function setRepositories(array $repositories): void| $repositories | array | name => iRepository. |
| returns | void | |
| throws | | if any value isn't an iRepository. |
protected function uniqueConflict(
MMO\core\server\Route $route,
MMO\core\interfaces\iCollection $collection,
array $keys,
MMO\core\interfaces\iEntity $other
): MMO\core\exceptions\eServer| $route | Route | |
| $collection | iCollection | |
| $keys | array | the @unique group whose values $other has |
| $other | iEntity | |
| returns | eServer | the 409 Conflict of checkUnique and checkUniqueChanges: a user's message (the error window of mmo.client shows it as it is), the fields by their title, translated; the collection and the entity that holds the values are logged (my.oviepro #282) |
Run a write's $fn: when it throws, the entities it changed are ignored, the commit of something else later in the request (a journal, a log) not saving half of a write refused
protected function writeAll(callable $fn): array