---
title: "Asking the installation"
description: "Three of this server's answers are not its own."
canonical: asking-the-installation.html
---

<a id="asking-the-installation"></a>

# Asking the installation

- [The order](#the-order)
- [The probe](#the-probe)
- [The three states](#the-three-states)
- [What an answer owes](#what-an-answer-owes)
- [Checking it by hand](#checking-it-by-hand)

Three of this server's answers are not its own. They belong to the installation
the caller stands in, and no bundled snapshot could be right about them. This
page is the order the server looks those answers up in, and what each step can
and cannot see. It says what an answer has to say when it came from the wrong
one.

The rules stay in [AGENTS.md](https://github.com/TYPO3/dev-companion/blob/main/AGENTS.md); what a change assumed is in
[decisions/discovery/](https://github.com/TYPO3/dev-companion/blob/main/decisions/discovery/readme.md). This is the
procedure.

![One square per entry the registry could return. The console path and the booted runtime return all of them. The package-file fallback returns every declared entry and none of the ones registered at runtime, and the answer states that.](../images/installation-fallback.svg)

<a id="the-order"></a>

## The order

1. **The console, where a command exists.** `Typo3Cli::run()` invokes the
   installation's own `bin/typo3`, through DDEV where the project runs there,
   for the registries TYPO3 exposes a command for. That is
   `language:domain:search` everywhere, and `fluid:namespaces` on TYPO3 14
   and up, where it exists.
2. **The container, where none does, or where the command answers less than the
   registry.** `Typo3Runtime::ask()` boots the installation in a subprocess
   and reads the registry itself. This is the only source that knows what a
   package registers dynamically. The backend modules are the second case.
   `debug:backend:modules` exports neither the navigation component a module
   resolves to nor its routes. It is TYPO3 v14 and up while this server answers
   for two lines below that (`D-ANS-077`). The effective configuration is the
   third. `configuration:show` arrived in TYPO3 14.2, so a console answer
   would leave 12.4 and 13.4 with the console's own "command is not defined"
   (`D-ANS-052`). The Fluid namespaces are the fourth. `fluid:namespaces`
   arrived in TYPO3 14.2 and the files it would fall back to arrived with it. So
   below that the container reads `SYS/fluid/namespaces`, which is where those
   versions keep the registry (`D-ANS-136`).

   The probe reads two topics only where a caller asked for them, because each
   costs what no other read wants. The whole of `TYPO3_CONF_VARS` is around 50
   kB of JSON. The resolution of one `type=flex` column runs four events, a
   file read and the TCA preparation behind it. What the caller asked them with
   goes into the payload the way the autoloader does, as one array. A read taken
   before goes, so no caller has to ask its topic first.
3. **The files, where the server can reach neither.** The registration files the
   packages ship, parsed and never included. Exact for everything declared,
   silent about everything else — and an answer that came from here says so.

Nothing in the chain runs in this process. Two Composer autoloaders under one
set of class names would take the whole MCP session down instead of one answer.
So would a platform check that fails on a PHP this machine does not have. A
subprocess brings its own interpreter and fails as an exit code.

<a id="the-probe"></a>

## The probe

[src/Installation/probe.php](https://github.com/TYPO3/dev-companion/blob/main/src/Installation/probe.php) goes in as
text, never as an include. `Typo3Runtime` strips its opening tag, writes the
installation's declared autoloader path into it, and hands it to
`Typo3Cli::php()`, which delivers it as:

```text
<interpreter> -r 'eval(base64_decode("<payload>"));'
```

Three details carry weight, and each of them cost a measurement:

- **The interpreter comes from the resolved console.** Directly it is the PHP
  that satisfied the installation's platform requirement. Under DDEV it is
  `ddev exec -- php`. Behind a stated `TYPO3_DEV_COMPANION_CONSOLE` the
  transport stays and only the binary changes: `ddev exec .build/bin/typo3`
  becomes `ddev exec php`. Where the server can derive no interpreter, that is
  the reason the answer carries.
- **The payload is base64-encoded.** `ddev exec` joins its arguments and hands
  the line to `bash`, so a payload passes through a shell nobody controls from
  here. Encoded, it carries no character that shell could act on. Bash expands
  raw PHP with a `$` in it before PHP ever sees it.
- **The autoloader path is relative.** The two sides of DDEV share no absolute
  path: the subprocess starts in the installation root, and inside the container
  that same root is `/var/www/html`.

The probe prints one JSON object on stdout and nothing else. It discards TYPO3's
own output buffer first, because an extension that echoes during boot would
otherwise sit in front of the payload.

<a id="the-three-states"></a>

## The three states

| State | What it means | What is done with it |
| --- | --- | --- |
| `full` | The container came up with every extension in it | It is the answer, and it is remembered for the session |
| `failsafe` | TYPO3 booted without essential configuration: core packages only | Never handed on. The files answer, and the reason travels with them |
| `unreachable` | No console, no interpreter, or the boot failed | Same — the files answer, with the reason |

**Failsafe is the state to know.** `Bootstrap::init()` turns it on when
`checkIfEssentialConfigurationExists()` fails, which is the ordinary condition
of an extension repository. `composer install` has run, there is no
`settings.php`, and there is no database. Every registry still answers,
`isLoaded()` still says `true` for the extension under work, and what comes
back is a core-only subset that looks complete. Measured against
`georgringer/news`: 1259 icons, not one of them the extension's own.

The server remembers only a `full` read. A caller that reads "the DDEV project
is stopped", starts it, and asks again must get the better answer in the same
session. That is the same rule `Typo3Cli::resolve()` and
`Instance::describe()` follow.

<a id="what-an-answer-owes"></a>

## What an answer owes

Every answer says which source it came from, in one vocabulary: `answeredBy`
is `installation` when the installation itself answered and `packages` when
its files did. Where it is `packages`, the answer also states what that leaves
out and why the read went that way. It says so in the text and not only in the
data. A caller that reads the matches would skip a line of its own. A registry
that reads as complete is what makes a review report defects nobody has.

The reason is half of it. The other half is which files it cost. A section a
tool leaves out because it is empty says the same nothing in two cases. The file
does not exist, or it exists and builds its list while it runs. Only the second
is a casualty of the degradation. `typo3_extension_describe` carries those in
`notReadStatically` and names them in its text; anything else that parses a
declaration file owes its callers the same distinction.

<a id="checking-it-by-hand"></a>

## Checking it by hand

The suite cannot boot TYPO3, since this repository has no core and never will.
So `Typo3RuntimeTest` holds everything around the boot. That the payload
reaches an interpreter and answers as data, and that the autoloader path is the
declared one. That every state that is not `full` arrives as a reason. A check
of the boot itself runs by hand against a set-up installation:

```bash
php -r '
    require "vendor/autoload.php";
    TYPO3\DevCompanion\Installation\Instance::discoverFrom("/path/to/a/site");
    $answer = TYPO3\DevCompanion\Installation\Typo3Runtime::ask();
    printf("%s %s\n", $answer["state"], $answer["reason"]);
    print_r(array_map("count", $answer["topics"]));
'
```

A site with an extension that registers at runtime is what makes the check worth
a run. If its identifiers are in the topic, the container answered.
