docs: rewrite README + add comprehensive docs/
README is now tight and link-heavy: 60-second tour, then deep links into docs/. The wall of detail moved out. docs/ covers the framework end-to-end: - getting-started.md — prerequisites by distro (Tumbleweed, Fedora, Debian/Ubuntu, Arch), full first-run walkthrough, troubleshooting. - architecture.md — process pair, transport, dev/bundled mode. - update-semantics.md — state machine + optimistic mutations + key round-tripping. - reactive-models.md — ReactiveListModel, ReactiveObject, Mercure dual-publish. - makers.md — make:bridge:resource/command/window. - dev-workflow.md — hot reload (PHP + QML), dev console, editor configs, bridge:doctor, snapshot/integration test loops, perfsmoke. - bundled-mode.md — supervisor, per-session secret rotation, first-launch migrations, auto-update wiring. - packaging-linux.md — make appimage, build-appimage.sh CLI, AppImageUpdate sidecar, perfsmoke budgets, release CI, bundle-size breakdown. - qml-api.md / php-api.md — exhaustive symbol reference with all Q_PROPERTY/Q_INVOKABLE/signals + every public PHP service / attribute / command. - configuration.md — every env var (host, Symfony, dev script, packaging script, perfsmoke), every CLI flag (php-qml-init, build-appimage.sh), make targets, default ports/paths. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
213
docs/makers.md
Normal file
213
docs/makers.md
Normal file
@@ -0,0 +1,213 @@
|
||||
# Makers
|
||||
|
||||
php-qml ships three [`symfony/maker-bundle`](https://symfony.com/bundles/SymfonyMakerBundle/current/index.html) makers. They generate the cross-side wiring (entity + controller + QML) for the common shapes — that's how a non-trivial app's PHP↔QML glue stays effectively zero.
|
||||
|
||||
All three are invoked from `symfony/`:
|
||||
|
||||
```bash
|
||||
cd symfony
|
||||
bin/console make:bridge:resource <Name>
|
||||
bin/console make:bridge:command <Name>
|
||||
bin/console make:bridge:window <Name>
|
||||
```
|
||||
|
||||
The maker bundle is `require-dev`, so production AppImage builds (which use `composer install --no-dev`) intentionally skip it. Run makers in development; check the output into git.
|
||||
|
||||
## `make:bridge:resource`
|
||||
|
||||
The headline maker: a Doctrine entity, REST controller, and starter QML list — all three reference each other correctly out of the box.
|
||||
|
||||
```bash
|
||||
bin/console make:bridge:resource Todo
|
||||
# created: src/Entity/Todo.php
|
||||
# created: src/Controller/TodoController.php
|
||||
# created: ../qml/TodoList.qml
|
||||
```
|
||||
|
||||
### Generated files
|
||||
|
||||
#### `src/Entity/<Name>.php`
|
||||
|
||||
```php
|
||||
#[ORM\Entity]
|
||||
#[BridgeResource] // ← marker the Doctrine subscriber looks for
|
||||
class Todo
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\Column(type: 'uuid')]
|
||||
private Uuid $id;
|
||||
|
||||
#[ORM\Column(length: 255)]
|
||||
private string $title = '';
|
||||
|
||||
#[ORM\Column(type: 'boolean')]
|
||||
private bool $done = false;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->id = Uuid::v7(); // UUIDv7 — k-sortable + URL-safe
|
||||
}
|
||||
|
||||
// generated getters/setters …
|
||||
}
|
||||
```
|
||||
|
||||
The `#[BridgeResource]` attribute is what makes the Doctrine subscriber dual-publish on `postPersist` / `postUpdate` / `postRemove`. Nothing else is auto-magic — you can add additional fields, relations, validators, lifecycle callbacks; the subscriber just looks for the attribute.
|
||||
|
||||
Pass `--int-id` to swap UUIDv7 for an auto-increment integer:
|
||||
|
||||
```bash
|
||||
bin/console make:bridge:resource Todo --int-id
|
||||
```
|
||||
|
||||
When to use which:
|
||||
- **UUIDv7 (default)** — distributed-friendly, exposes no insertion-order leak, sortable. Good for anything an end-user might sync between machines.
|
||||
- **Integer ID** — easier to debug from a shell; smaller wire size; no client-side ID generation needed. Pick this for purely-local single-machine apps.
|
||||
|
||||
#### `src/Controller/<Name>Controller.php`
|
||||
|
||||
CRUD endpoints on `/api/<lowercase-name>`:
|
||||
|
||||
| Verb | Path | Behaviour |
|
||||
| --- | --- | --- |
|
||||
| `GET` | `/api/todos` | List collection. |
|
||||
| `POST` | `/api/todos` | Create. |
|
||||
| `GET` | `/api/todos/<id>` | Fetch single. |
|
||||
| `PATCH` | `/api/todos/<id>` | Partial update. |
|
||||
| `DELETE` | `/api/todos/<id>` | Delete. |
|
||||
|
||||
The controller injects `EntityManagerInterface` and `NormalizerInterface` (not `SerializerInterface` — Symfony's interface lacks `normalize()`). It uses Symfony's normalizer to JSON-encode entities; `BridgeResource` entities serialise their fields directly.
|
||||
|
||||
Routes are picked up automatically — `framework/skeleton/config/routes.yaml` declares the bundle's controller directory as a route resource, so generated controllers light up without further configuration.
|
||||
|
||||
#### `qml/<Name>List.qml`
|
||||
|
||||
A starter `ListView` driven by a `ReactiveListModel`:
|
||||
|
||||
```qml
|
||||
ReactiveListModel {
|
||||
id: model
|
||||
baseUrl: BackendConnection.url
|
||||
token: BackendConnection.token
|
||||
source: "/api/todos"
|
||||
topic: "app://model/todo"
|
||||
}
|
||||
|
||||
ListView {
|
||||
model: model
|
||||
delegate: ItemDelegate {
|
||||
required property string id
|
||||
required property string title
|
||||
// … fields per entity
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Use it from `Main.qml`:
|
||||
|
||||
```qml
|
||||
import Todo // local QML module declared by your CMakeLists.txt
|
||||
TodoList { anchors.fill: parent }
|
||||
```
|
||||
|
||||
### After a `resource` maker
|
||||
|
||||
```bash
|
||||
bin/console make:migration
|
||||
bin/console doctrine:migrations:migrate -n
|
||||
```
|
||||
|
||||
Without that the entity is in the metadata but not in the schema, and the first GET will fail with `no such table`.
|
||||
|
||||
### Snapshot test
|
||||
|
||||
`framework/php/tests/snapshot/` carries a frozen snapshot of the maker's output for a `Todo` resource. The snapshot test in CI re-runs the maker into a temp dir and diffs against the snapshot — if the maker drifts (e.g. an unrelated change breaks the template), CI catches it.
|
||||
|
||||
If you intentionally change the maker, regenerate the snapshot:
|
||||
|
||||
```bash
|
||||
cd framework/php
|
||||
bin/run-snapshot.sh # see tests/snapshot/run.sh
|
||||
git add tests/snapshot/
|
||||
```
|
||||
|
||||
## `make:bridge:command`
|
||||
|
||||
Generates a controller stub for a non-CRUD action — the kind of thing you'd write a Service Layer or Application Service for in a vanilla Symfony app.
|
||||
|
||||
```bash
|
||||
bin/console make:bridge:command MarkAllDone
|
||||
# created: src/Controller/MarkAllDoneController.php
|
||||
```
|
||||
|
||||
The generated controller:
|
||||
|
||||
```php
|
||||
#[Route('/api/mark-all-done', methods: ['POST'])]
|
||||
final class MarkAllDoneController
|
||||
{
|
||||
public function __construct(private EntityManagerInterface $em) {}
|
||||
|
||||
public function __invoke(Request $request): JsonResponse
|
||||
{
|
||||
// TODO: your business logic
|
||||
$this->em->flush();
|
||||
return new JsonResponse(['ok' => true]);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Fill in the body. Any `#[BridgeResource]` entities you mutate inside the action publish their Mercure events as usual — multi-row commands reuse the same `correlationKey` (from the request's `Idempotency-Key`), so QML clients see one logical mutation completing.
|
||||
|
||||
The route path is derived by kebab-casing the maker name (`MarkAllDone` → `/api/mark-all-done`). Override it manually if you want a different path.
|
||||
|
||||
### When to use a command vs a CRUD endpoint
|
||||
|
||||
CRUD covers the 80%. Reach for a command when:
|
||||
|
||||
- The verb isn't a primitive (e.g. *publish*, *retry*, *archive*).
|
||||
- The action affects multiple resources atomically (e.g. *mark all done* across a collection).
|
||||
- You need request-side validation that doesn't fit the entity (e.g. *only the owner can do this*).
|
||||
|
||||
## `make:bridge:window`
|
||||
|
||||
Generates a second-window scaffold. Useful when an app is fundamentally multi-window (settings dialog, detail pop-out, second viewport).
|
||||
|
||||
```bash
|
||||
bin/console make:bridge:window Settings
|
||||
# created: ../qml/SettingsWindow.qml
|
||||
```
|
||||
|
||||
The generated QML window:
|
||||
- Imports `PhpQml.Bridge`.
|
||||
- Wraps content in `AppShell` (so it shows the same Reconnecting / Offline chrome as the main window).
|
||||
- Has its own RestClient + a `Connections { target: SingleInstance ... }` block for launch-arg forwarding.
|
||||
|
||||
Open it from your `Main.qml`:
|
||||
|
||||
```qml
|
||||
Component {
|
||||
id: settingsCmp
|
||||
SettingsWindow {}
|
||||
}
|
||||
|
||||
Button {
|
||||
text: "Settings"
|
||||
onClicked: settingsCmp.createObject().show()
|
||||
}
|
||||
```
|
||||
|
||||
The window owns its own state — including any `ReactiveListModel`/`ReactiveObject` instances. Two windows of the same app remain coherent through the Mercure dual-publish, not through any inter-window IPC. See [Reactive models §multi-window coherence](update-semantics.md#multi-window-coherence).
|
||||
|
||||
## Conventions the makers follow
|
||||
|
||||
- **Snake/Pascal/lowercase derivation.** A `Todo` resource produces `Todo` (entity), `TodoController`, `TodoList.qml`, `/api/todos` (lowercased + plural). `MarkAllDone` produces `MarkAllDoneController` and `/api/mark-all-done`. Hyphens in maker names aren't accepted — use PascalCase.
|
||||
- **Files land where Symfony expects.** Entities under `src/Entity/`, controllers under `src/Controller/`. QML files land under `../qml/` relative to the Symfony app — i.e. the project's QML source tree.
|
||||
- **Idempotent.** Re-running a maker with the same name is a soft error (file exists). Delete the file first if you want a fresh template.
|
||||
- **Generated code is yours.** Edit it. The makers don't keep round-tripping through it. They are scaffolders, not source-of-truth generators.
|
||||
|
||||
## See also
|
||||
|
||||
- [Reactive models](reactive-models.md) — what the generated `<Name>List.qml` plugs into.
|
||||
- [Update semantics](update-semantics.md) — what `correlationKey` does after the maker's controller `flush()`es.
|
||||
- [PHP API](php-api.md#bridgeresource-attribute) — `#[BridgeResource]` attribute.
|
||||
Reference in New Issue
Block a user