API / Minn

Minn (root namespace)

The root namespace: the front door, the autoloader, the one database door, the REST error. Every other namespace sits one level below it. 8 classes, 55 public methods and 21 internals. This page is the public surface; each class name opens the whole class.

Autoloader final class

public/minn/src/Minn/Autoloader.php · 16 lines · Open the class →

PSR-4 for the Minn namespace: Minn\Http\Request lives at src/Minn/Http/Request.php.

staticregister(): void

Registers the PSR-4 loader for the Minn namespace.

Context final readonly class

public/minn/src/Minn/Context.php · 41 lines · Open the class →

One request, as a value: the database door, who is asking, what they may do, and where the site's files are. It is built once at the front door and handed down, so nothing below has to reach for a global to learn what request it is answering.

The reader is the one part that differs by surface (REST resolves it from the nonce-bound caller, the front from the session cookie), so a context is made with the reader its surface resolved.

new__construct(Minn\Db $db, Minn\Content\Site $site, ?Minn\Http\Request $request, Minn\Content\Reader $reader, Minn\Auth\Capabilities $capabilities, string $engineDir, string $absPath, string $version)
propreadonly Minn\Db $db
propreadonly Minn\Content\Site $site
propreadonly ?Minn\Http\Request $request
propreadonly Minn\Content\Reader $reader
propreadonly Minn\Auth\Capabilities $capabilities
propreadonly string $engineDir

the minn/ folder: the engine's own files

propreadonly string $absPath

the site root with a trailing slash, as ABSPATH holds it

propreadonly string $version

the WordPress release whose contracts the runtime speaks

methodwithReader(Minn\Content\Reader $reader): self

The same context with another reader, for a surface that resolves one later.

methodcontentDir(): string

wp-content under the site root.

methodthemesDir(): string

The themes folder under wp-content.

methodisSecure(): bool

Whether the request is over HTTPS.

Db final class

public/minn/src/Minn/Db.php · 370 lines · Open the class →

The one door to the database. Every query is a prepared statement; the placeholder types are derived from the PHP values, so callers pass plain arrays and never spell out "issd". A parameter that is a list stands for a list of values: its "?" becomes as many placeholders as the list is long, so "post_type IN (?)" takes the types themselves.

constINCOMPATIBLE_MODES = array ( 0 => 'NO_ZERO_DATE', 1 => 'ONLY_FULL_GROUP_BY', 2 => 'STRICT_TRANS_TABLES', 3 => 'STRICT_ALL_TABLES', 4 => 'TRADITIONAL', 5 => 'ANSI', )

The SQL modes WordPress turns off for its session (wpdb's incompatible modes): strictness and the modes that change what queries mean.

new__construct(mysqli $connection, string $prefix)
staticcolumnLength(string $type): array|false

The most a column of a MySQL type holds, as wpdb::get_col_length answers: characters for char and varchar, bytes for the binary, blob and text types; false for any other type.

@return array{type: string, length: int}|false
staticcurrent(): self

The connection the request being answered speaks through: its runtime's own, so nothing deep in a render can reach a connection belonging to another request. With no runtime (the command line, the unit suite) it is the process's own connection.

This is the one place in the core that asks the runtime anything; the alternative is a database door threaded through every renderer, and the seam is here until that threading is done.

staticshared(): self

The one connection for this process, opened from wp-config's constants on first use, its session without the incompatible modes, as WordPress's own connection is (a plugin's insert that leaves out a text column then succeeds, as it does there).

staticsessionModes(string $modes): string

The session's modes: the server's, less the reference's incompatible ones and NO_BACKSLASH_ESCAPES, which the engine's own literal escaping (Query\Sql) relies on being off. Plugins see only the reference's list (incompatible_sql_modes).

staticcompatibleModes(string $modes, array $incompatible): string

A comma-separated SQL mode list without the incompatible ones (any case).

@param list<string> $incompatible
methodsetModes(string $modes): void

Sets this session's SQL modes, never NO_BACKSLASH_ESCAPES: the engine's literals escape quotes with a backslash (Query\Sql), which that mode would turn into a plain character, so a set_sql_mode() asking for it gets every other mode it named.

methodconnection(): mysqli

The underlying mysqli handle, for the facade's wpdb.

methodescape(string $value): string

A value escaped for direct interpolation into SQL, for callers that build their own statements.

methodprefix(): string

The table prefix from wp-config.

methodtable(string $name): string

`posts` becomes `wp_posts`; the prefix comes from wp-config.php.

methodrows(string $sql, array $params = array ( )): array

Every row of a prepared query, as associative arrays.

@return list<array<string, mixed>>
methodrow(string $sql, array $params = array ( )): ?array

The first row of a prepared query, or null.

@return array<string, mixed>|null
methodvalue(string $sql, array $params = array ( )): string|int|float|null

The first column of the first row, or null when there is no row.

methodexecute(string $sql, array $params = array ( )): int

Runs a write and returns the affected row count.

staticonWrite(string $table, Closure $listener): void

Asks to be told whenever a write statement names a table (Content\RowCache drops what it kept from it).

staticwrote(string $sql): void

Tells the listeners of every table a write statement names, whichever door ran it (this one, or the facade's wpdb for plugin code). A name that merely contains another (wp_postmeta for posts) is not it.

staticexpand(string $sql, array $params): array

Rewrites every "?" whose parameter is a list into that many placeholders, and flattens the values to match, so "IN (?)" becomes "IN (?, ?, ?)". An empty list becomes NULL, which nothing is IN, so a caller that means "everything" when the list is empty says so itself.

@param list<mixed> $params
@return array{0: string, 1: list<mixed>}
methodtransaction(callable $work): mixed

Runs a unit of work as one transaction: everything the closure writes lands, or none of it does. The closure's return value is passed back.

A transaction already open is joined rather than nested, since the storage engine has no nested transactions; the outermost call is the one that commits. A table that cannot do transactions (MyISAM, which no WordPress core table uses) silently keeps each write, which is the behaviour there was before this existed.

@param callable(): T $work
methodinsertId(): int

The id the last INSERT produced.

methodoption(string $name): ?string

One option's stored value, or null when it is unset; home and siteurl answer to WP_HOME and WP_SITEURL. Once the runtime boots, the read is its option store's (the autoloaded list, wp_cache's options group), so the engine's own reads cost no query each, as get_option()'s don't.

staticreadOptionsThrough(?Closure $source): void

Where option reads come from from now on: the runtime's option store, or null for the row itself.

staticconfigured(string $name): ?string

What wp-config.php sets for an option, or null: WP_HOME and WP_SITEURL, when defined, stand for home and siteurl on every read, as on the reference (_config_wp_home), without a trailing slash.

methodstoredOption(string $name): ?string

One option's row as stored, whatever wp-config.php says, or null when it is unset.

insideplaceholders(), run()

Engine final readonly class

public/minn/src/Minn/Engine.php · 335 lines · Open the class →

The engine's front door. An unmodified wp-config.php ends by requiring wp-settings.php, which is the engine's own boot file; it hands off here. REST is dispatched first (from the path or from ?rest_route=), then the admin, the login endpoint, and finally the public site.

constWP_VERSION = '7.1.3'

The WordPress release whose contracts the runtime speaks; wp-includes/version.php says the same.

new__construct(string $version, string $engineDir)
methodserve(): void

The front door: answer the request that arrived and send it. This is the only place the engine touches the world, so everything under it is a function of a request, callable more than once in a process.

methodanswer(Minn\Http\Request $request): Minn\Http\Response

The response for one request, whatever happens: a database that cannot be reached, salts that are not set, or a failure anywhere underneath, each answered in the language the request asked in.

insiderestRoute(), bootRuntimeForRest(), recheckNonce(), adoptSettledUser(), markThemedRequest(), armRecoveryPause(), plain(), handle(), frontPipeline()

Html final class

public/minn/src/Minn/Html.php · 109 lines · Open the class →

HTML, the front door for escaping and cutting markup: plain static calls, one import away (use Minn\Html;), the way Minn\Http and Minn\Json are. Escaping at output boundaries goes through here.

constSPLIT = '/(<(?:!--.*?(?:-->|$)|!\\[CDATA\\[.*?(?:\\]\\]>|$)|[^>]*>?))/s'

Markup's pieces of HTML: comments and CDATA whole (to the end when unclosed), else from "<" to the next ">".

staticsplit(string $html): array

Markup cut into text and HTML pieces, the HTML ones at the odd positions (wp_html_split): a comment or CDATA section is one piece whatever it holds, and a "<" in text starts a piece that runs to the next ">".

@return list<string>
staticesc(?string $value): string

Text escaped for HTML.

staticattr(?string $value): string

Text escaped for an attribute value.

staticaddClasses(string $html, array $classes, ?string $onClass = NULL): string

Appends classes to the first element in a fragment (or to the first element already carrying $onClass), adding a class attribute when the element has none. Existing classes keep their order.

@param list<string> $classes
staticstripAllTags(string $text, bool $collapseWhitespace = false): string

Drops script and style elements with their contents, then every other tag; whitespace runs optionally collapse to one space.

statictextField(string $text, bool $keepNewlines, Closure $validUtf8, Closure $encodeLessThan): string

A single line of plain text from user input: tags stripped, control whitespace folded, percent escapes removed. The caller passes its own UTF-8 check and less-than encoder so its filters keep applying.

@param Closure(string): string $validUtf8 @param Closure(string): string $encodeLessThan
staticscriptBody(string $markup): ?string

The code inside markup that is exactly one bare script element once the surrounding whitespace is gone, or null for anything else: a tag with attributes, an empty element, a missing end. The tag names match in any case and the code is returned as written.

Http final readonly class

public/minn/src/Minn/Http.php · 184 lines · Open the class →

Outgoing HTTP, a client handed to the code that uses it. An extension finds one on $minn->http, bound to the hosts its minn.json asks for under needs.http; an engine class takes one in its constructor, the real network unless a test passes a fake's:

$data = $minn->http->get('https://api.example.com/status')->json(); $this->http->post($url, json: ['email' => $email], timeout: 10)->throw();

Every verb answers with an Exchange (->ok(), ->failed(), ->json(), ->header(), ->cookie(), ->throw(), ->code, ->body) and takes the same named arguments, each optional:

- `query`: array added to the URL's query string - `headers`: `name => value` (a list value repeats the header), or "Name: value" lines - `json`, `form`, `body` (post, put, patch): an array sent as JSON, an array form-encoded, or a string sent as it is; one of the three, and the first two set the Content-Type - `timeout`: seconds for the whole exchange, 5 by default - `redirects`: how many to follow, 5 by default; 0 hands back the 3xx - `hosts`: URL prefixes every hop must start with (`['https://api.github.com/']`) - `private`: hosts allowed to reach a private address. Loopback, LAN, link-local and cloud-metadata addresses are refused otherwise, at every hop and after DNS, so a URL someone typed is safe to fetch - `maxBytes`: the largest body accepted; a longer one fails the exchange - `userAgent`: Minn/{version} by default

A misspelt argument is an error at the line that has it. Credentials (Authorization, Cookie) are dropped when a redirect leaves the origin. A fake (new Http\Fake([...])->http()) answers only the client it made, so a test shares nothing with the rest of the process.

new__construct(array $within = array ( ), ?Minn\Http\Fake $fake = NULL, ?string $refusal = NULL)

The network, or $fake answering in its place. With $within, every hop of every request must also start with one of those URL prefixes, whatever a call's own hosts: say (an extension's needs.http).

@param list<string> $within
staticrefused(string $why): self

A client that throws $why at every call: what an extension holds when its minn.json did not ask for http.

methodget(string $url, array $query = array ( ), array $headers = array ( ), float $timeout = 5.0, int $redirects = 5, array $hosts = array ( ), array $private = array ( ), ?int $maxBytes = NULL, ?string $userAgent = NULL): Minn\Http\Exchange

A GET.

methodhead(string $url, array $query = array ( ), array $headers = array ( ), float $timeout = 5.0, int $redirects = 5, array $hosts = array ( ), array $private = array ( ), ?string $userAgent = NULL): Minn\Http\Exchange

A HEAD: the status and headers a GET would bring, without the body.

methoddelete(string $url, array $query = array ( ), array $headers = array ( ), float $timeout = 5.0, int $redirects = 5, array $hosts = array ( ), array $private = array ( ), ?int $maxBytes = NULL, ?string $userAgent = NULL): Minn\Http\Exchange

A DELETE.

methodpost(string $url, ?array $json = NULL, ?array $form = NULL, ?string $body = NULL, array $query = array ( ), array $headers = array ( ), float $timeout = 5.0, int $redirects = 5, array $hosts = array ( ), array $private = array ( ), ?int $maxBytes = NULL, ?string $userAgent = NULL): Minn\Http\Exchange

A POST with a json:, form: or body: payload.

methodput(string $url, ?array $json = NULL, ?array $form = NULL, ?string $body = NULL, array $query = array ( ), array $headers = array ( ), float $timeout = 5.0, int $redirects = 5, array $hosts = array ( ), array $private = array ( ), ?int $maxBytes = NULL, ?string $userAgent = NULL): Minn\Http\Exchange

A PUT with a json:, form: or body: payload.

methodpatch(string $url, ?array $json = NULL, ?array $form = NULL, ?string $body = NULL, array $query = array ( ), array $headers = array ( ), float $timeout = 5.0, int $redirects = 5, array $hosts = array ( ), array $private = array ( ), ?int $maxBytes = NULL, ?string $userAgent = NULL): Minn\Http\Exchange

A PATCH with a json:, form: or body: payload.

methodsend(Minn\Http\Outbound $request): Minn\Http\Exchange

Sends exactly what the Outbound says, with none of the verbs' rules about where a request may go: the door WordPress's own HTTP API comes through after applying its rules. A client bound to hosts refuses it, since curl follows a sent request's redirects where no rule can see.

insidefollow(), deliver(), throwIfRefused(), nextHop(), outbound(), payload(), lines(), withQuery()

Json final class

public/minn/src/Minn/Json.php · 134 lines · Open the class →

JSON in and out, for the engine and for code written for Minn:

use Minn\Json;

Json::encode($rows); // {"url":"https://example.com/a","name":"Café"} Json::decode($text); // objects as arrays; JsonException when it is not JSON Json::decode($stored, or: []); // the fallback, for text that may be empty or broken Json::file($path); // the same, read from a file; the error names the file

encode() writes slashes and characters as themselves unless flags say otherwise (Json::ESCAPED is the form WordPress writes, which REST bodies keep). A string cut mid-character still encodes: each invalid byte becomes "?", as wp_json_encode() does it (probe functions-utils). What cannot be encoded even then (NAN, nesting past the depth) throws a JsonException.

constREADABLE = 320

Slashes and non-ASCII characters written as themselves: encode()'s default.

constESCAPED = 0

Slashes and non-ASCII characters escaped, as wp_json_encode() and the REST API write them.

staticencode(mixed $value, int $flags = self::READABLE, int $depth = 512): string

A value as JSON; invalid UTF-8 is repaired, never a reason to fail.

staticdecode(string $text, mixed $or = NULL): mixed

The value the text holds, objects as arrays; with `or:` given, that comes back instead of an exception.

staticfile(string $path, mixed $or = NULL): mixed

The value a JSON file holds, objects as arrays; with `or:` given, a missing or broken file gives that instead.

staticrepair(mixed $value, int $depth = 512): mixed

A value with every string and key made valid UTF-8, each invalid byte becoming "?", objects read as plain objects of their public properties.

staticdecodeKeepingEmptyObjects(string $json): mixed

JSON as arrays, but an empty object stays an object: an in-process REST answer keeps `{}` where the reference's data holds an empty object (a global style's emptied settings), which decoding to arrays would turn into `[]`.

insidemember(), text()

RestError final class

public/minn/src/Minn/RestError.php · 60 lines · implements Stringable, Throwable · extends RuntimeException · Open the class →

A WordPress-shaped error, thrown from anywhere and rendered once by the kernel: {"code": ..., "message": ..., "data": {"status": ...}}.

new__construct(string $errorCode, string $message, int $status, array $extra = array ( ), array $topLevel = array ( ), bool $bare = false)
@param array<string, mixed> $extra keys added beside status inside data (the whole of data when it names status)
@param array<string, mixed> $topLevel keys added beside code/message/data
propreadonly string $errorCode
propreadonly int $status
propreadonly array $extra
propreadonly array $topLevel
staticnoRoute(): self

The reference's 404 for a route nothing answers.

staticmissingParams(array $params): self

The reference's 400 for required parameters that did not arrive.

@param list<string> $params
staticinvalidParam(string $param, string $message, string $code = 'rest_not_in_enum', mixed $data = NULL): self

The reference's 400 for one parameter its schema refuses: the message under params, and under details with the refusal's code and data, as WP_REST_Request::has_valid_params reports it.

staticbare(string $code, string $message): self

A statusless core error as REST serves it: HTTP 500 with data null.

methodpayload(): array

The error as the reference's JSON body: code, message, data.