v0.2.0 (7/N): make:bridge:event maker
Implements PLAN.md §8's third makers-table row. Single-command path
from a PHP domain event to a QML signal-handler:
- src/Event/<Name>Event.php — readonly value object stub
- src/EventSubscriber/<Name>Subscriber.php — listens to the event,
republishes via PublisherInterface on app://event/<kebab-name>
with op:"event"
- {qml_path}/<Name>EventHandler.qml — MercureClient bound to the
topic, re-emits the envelope's data as a typed signal
Stub uses an `array $payload` field so the user can substitute typed
properties for whatever shape they need. Subscriber example uses the
PublisherInterface contract from chunk 1; QML stub uses MercureClient
+ BackendConnection both already shipping.
Wired into services.yaml's when@dev block (autoconfigure picks up
maker.command tag, same pattern as existing BridgeResourceMaker /
BridgeWindowMaker). Three new snapshot baselines plus a snapshot
runner extension exercising the new maker against the same Todo /
TodoCompleted naming the existing baselines use.
End-to-end verified locally: maker output matches baselines, dev
container compiles, listing make:bridge:* shows the new command.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -16,6 +16,7 @@ This section tracks work landing on `dev` toward **v0.2.0** (next minor; pre-1.0
|
|||||||
- **`Maker\Support\NameInput`** — shared interactive name prompt. All three `make:bridge:*` makers (`resource`, `command`, `window`) re-implemented the same "prompt, trim, ucfirst, reject empty" closure inline; collapsed into one call site so empty-argument and validation behaviour stay in lockstep.
|
- **`Maker\Support\NameInput`** — shared interactive name prompt. All three `make:bridge:*` makers (`resource`, `command`, `window`) re-implemented the same "prompt, trim, ucfirst, reject empty" closure inline; collapsed into one call site so empty-argument and validation behaviour stay in lockstep.
|
||||||
- **`Maker\Support\Naming`** — `camelTo($name, $separator)` helper. Replaces inline `preg_replace('/(?<!^)[A-Z]/', $sep.'$0', $name)` regex copies (BridgeResourceMaker emits `_`-joined route plurals, BridgeCommandMaker emits `-`-joined kebab slugs).
|
- **`Maker\Support\Naming`** — `camelTo($name, $separator)` helper. Replaces inline `preg_replace('/(?<!^)[A-Z]/', $sep.'$0', $name)` regex copies (BridgeResourceMaker emits `_`-joined route plurals, BridgeCommandMaker emits `-`-joined kebab slugs).
|
||||||
- **`make:bridge:resource --with-dto` opt-in.** Generates `Create<Name>Dto` + `Update<Name>Dto` under `src/Dto/` alongside the controller, and the controller dispatches via `#[MapRequestPayload]`. Closes the input-validation gap from the audit: malformed JSON, missing required fields, or `#[Assert\NotBlank]` violations now produce RFC 7807 `application/problem+json` automatically (Symfony's `RequestPayloadValueResolver`) — no more `if (isset($data['title']))` boilerplate, no silent type coercion. Update DTOs use nullable defaults so PATCH callers send only the fields they want changed. Without `--with-dto` the legacy template still ships unchanged. Maker fails loud if `symfony/validator` isn't autoloadable. Skeleton + example/todo composer.json pull `symfony/validator` so scaffolded apps work out of the box. Snapshot test exercises both modes.
|
- **`make:bridge:resource --with-dto` opt-in.** Generates `Create<Name>Dto` + `Update<Name>Dto` under `src/Dto/` alongside the controller, and the controller dispatches via `#[MapRequestPayload]`. Closes the input-validation gap from the audit: malformed JSON, missing required fields, or `#[Assert\NotBlank]` violations now produce RFC 7807 `application/problem+json` automatically (Symfony's `RequestPayloadValueResolver`) — no more `if (isset($data['title']))` boilerplate, no silent type coercion. Update DTOs use nullable defaults so PATCH callers send only the fields they want changed. Without `--with-dto` the legacy template still ships unchanged. Maker fails loud if `symfony/validator` isn't autoloadable. Skeleton + example/todo composer.json pull `symfony/validator` so scaffolded apps work out of the box. Snapshot test exercises both modes.
|
||||||
|
- **`make:bridge:event <Name>` maker.** Generates a domain-event class (`src/Event/<Name>Event.php`, readonly value object), a subscriber (`src/EventSubscriber/<Name>Subscriber.php`) that republishes via `PublisherInterface` on `app://event/<kebab-name>`, and a QML stub (`{qml_path}/<Name>EventHandler.qml`) that listens via `MercureClient` and re-emits as a typed `signal`. Closes the third row of PLAN.md §8's makers table; pairs with the existing `make:bridge:resource` / `command` / `window` makers so domain events have a one-command path from PHP through to QML.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
|||||||
@@ -41,3 +41,7 @@ when@dev:
|
|||||||
PhpQml\Bridge\Maker\BridgeWindowMaker:
|
PhpQml\Bridge\Maker\BridgeWindowMaker:
|
||||||
arguments:
|
arguments:
|
||||||
$qmlPath: '%bridge.qml_path%'
|
$qmlPath: '%bridge.qml_path%'
|
||||||
|
|
||||||
|
PhpQml\Bridge\Maker\BridgeEventMaker:
|
||||||
|
arguments:
|
||||||
|
$qmlPath: '%bridge.qml_path%'
|
||||||
|
|||||||
140
framework/php/src/Maker/BridgeEventMaker.php
Normal file
140
framework/php/src/Maker/BridgeEventMaker.php
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace PhpQml\Bridge\Maker;
|
||||||
|
|
||||||
|
use PhpQml\Bridge\Maker\Support\NameInput;
|
||||||
|
use PhpQml\Bridge\Maker\Support\Naming;
|
||||||
|
use PhpQml\Bridge\PublisherInterface;
|
||||||
|
use Symfony\Bundle\MakerBundle\ConsoleStyle;
|
||||||
|
use Symfony\Bundle\MakerBundle\DependencyBuilder;
|
||||||
|
use Symfony\Bundle\MakerBundle\Generator;
|
||||||
|
use Symfony\Bundle\MakerBundle\InputConfiguration;
|
||||||
|
use Symfony\Bundle\MakerBundle\Maker\AbstractMaker;
|
||||||
|
use Symfony\Bundle\MakerBundle\Str;
|
||||||
|
use Symfony\Component\Console\Command\Command;
|
||||||
|
use Symfony\Component\Console\Input\InputArgument;
|
||||||
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
|
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `make:bridge:event <Name>` — generates the three files that wire a
|
||||||
|
* domain event onto the bridge's `app://event/{name}` Mercure topic:
|
||||||
|
*
|
||||||
|
* - src/Event/<Name>Event.php — readonly event value object
|
||||||
|
* - src/EventSubscriber/<Name>Subscriber.php — listens, republishes via PublisherInterface
|
||||||
|
* - {qml_path}/<Name>EventHandler.qml — QML stub that re-emits as a typed signal
|
||||||
|
*
|
||||||
|
* Topic shape per PLAN.md §4: `app://event/<kebab-case-of-name>`. The
|
||||||
|
* generated stub publishes envelopes with `op: "event"` so QML clients
|
||||||
|
* can dispatch on op alongside `upsert` / `delete` / `replace`.
|
||||||
|
*/
|
||||||
|
final class BridgeEventMaker extends AbstractMaker
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly string $qmlPath = '../qml/',
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getCommandName(): string
|
||||||
|
{
|
||||||
|
return 'make:bridge:event';
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getCommandDescription(): string
|
||||||
|
{
|
||||||
|
return 'Generate a domain event class + subscriber + QML handler stub.';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function configureCommand(Command $command, InputConfiguration $inputConfig): void
|
||||||
|
{
|
||||||
|
$command
|
||||||
|
->addArgument(
|
||||||
|
'name',
|
||||||
|
InputArgument::OPTIONAL,
|
||||||
|
'CamelCase event name (e.g. TodoCompleted, ImportFinished).',
|
||||||
|
)
|
||||||
|
->setHelp(
|
||||||
|
"Creates three files:\n\n"
|
||||||
|
." • <info>src/Event/<Name>Event.php</info> — readonly event value object\n"
|
||||||
|
." • <info>src/EventSubscriber/<Name>Subscriber.php</info> — republishes on app://event/<kebab-name>\n"
|
||||||
|
." • <info>{qml_path}/<Name>EventHandler.qml</info> — QML stub re-emitting as a typed signal\n\n"
|
||||||
|
."Dispatch from PHP with <info>\$dispatcher->dispatch(new <Name>Event([...]))</info>.\n"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function interact(InputInterface $input, ConsoleStyle $io, Command $command): void
|
||||||
|
{
|
||||||
|
NameInput::askOrFail(
|
||||||
|
$input,
|
||||||
|
$io,
|
||||||
|
'name',
|
||||||
|
'Event name (CamelCase, e.g. TodoCompleted)?',
|
||||||
|
'Event name cannot be empty.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator): void
|
||||||
|
{
|
||||||
|
$rawName = (string) $input->getArgument('name');
|
||||||
|
$singular = ucfirst(Str::asCamelCase($rawName));
|
||||||
|
$topic = Naming::camelTo($singular, '-');
|
||||||
|
|
||||||
|
$eventFqcn = $generator->createClassNameDetails(
|
||||||
|
$singular,
|
||||||
|
'Event\\',
|
||||||
|
'Event',
|
||||||
|
);
|
||||||
|
$subscriberFqcn = $generator->createClassNameDetails(
|
||||||
|
$singular,
|
||||||
|
'EventSubscriber\\',
|
||||||
|
'Subscriber',
|
||||||
|
);
|
||||||
|
|
||||||
|
$vars = [
|
||||||
|
'singular' => $singular,
|
||||||
|
'event_topic' => $topic,
|
||||||
|
'event_short' => $eventFqcn->getShortName(),
|
||||||
|
'event_fqcn' => $eventFqcn->getFullName(),
|
||||||
|
'subscriber_short' => $subscriberFqcn->getShortName(),
|
||||||
|
'subscriber_fqcn' => $subscriberFqcn->getFullName(),
|
||||||
|
'handler_method' => 'on'.$singular,
|
||||||
|
'signal_name' => lcfirst($singular),
|
||||||
|
];
|
||||||
|
|
||||||
|
$generator->generateFile(
|
||||||
|
'src/Event/'.$eventFqcn->getShortName().'.php',
|
||||||
|
__DIR__.'/templates/EventClass.tpl.php',
|
||||||
|
$vars,
|
||||||
|
);
|
||||||
|
$generator->generateFile(
|
||||||
|
'src/EventSubscriber/'.$subscriberFqcn->getShortName().'.php',
|
||||||
|
__DIR__.'/templates/EventSubscriber.tpl.php',
|
||||||
|
$vars,
|
||||||
|
);
|
||||||
|
|
||||||
|
$qmlTarget = rtrim($this->qmlPath, '/').'/'.$singular.'EventHandler.qml';
|
||||||
|
$generator->generateFile(
|
||||||
|
$qmlTarget,
|
||||||
|
__DIR__.'/templates/EventHandlerQml.tpl.php',
|
||||||
|
$vars,
|
||||||
|
);
|
||||||
|
|
||||||
|
$generator->writeChanges();
|
||||||
|
|
||||||
|
$this->writeSuccessMessage($io);
|
||||||
|
$io->text([
|
||||||
|
'Next:',
|
||||||
|
" • Replace the <info>payload</info> array on <info>{$eventFqcn->getShortName()}</info> with typed properties.",
|
||||||
|
" • Dispatch via <info>\$dispatcher->dispatch(new {$eventFqcn->getShortName()}(\$data))</info>.",
|
||||||
|
" • Use <info>{$singular}EventHandler.qml</info> in your QML to receive the echo.",
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function configureDependencies(DependencyBuilder $dependencies): void
|
||||||
|
{
|
||||||
|
$dependencies->addClassDependency(EventSubscriberInterface::class, 'symfony/event-dispatcher');
|
||||||
|
$dependencies->addClassDependency(PublisherInterface::class, 'php-qml/bridge');
|
||||||
|
}
|
||||||
|
}
|
||||||
22
framework/php/src/Maker/templates/EventClass.tpl.php
Normal file
22
framework/php/src/Maker/templates/EventClass.tpl.php
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<?= "<?php\n" ?>
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Event;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Domain event published on `app://event/<?= $event_topic ?>` by
|
||||||
|
* <?= $subscriber_short ?>.
|
||||||
|
*
|
||||||
|
* Auto-generated stub — replace the `payload` field with typed
|
||||||
|
* properties matching the event you actually fire.
|
||||||
|
*/
|
||||||
|
final readonly class <?= $event_short ?>
|
||||||
|
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
/** @var array<string, mixed> */
|
||||||
|
public array $payload = [],
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
31
framework/php/src/Maker/templates/EventHandlerQml.tpl.php
Normal file
31
framework/php/src/Maker/templates/EventHandlerQml.tpl.php
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
// Auto-generated by `bin/console make:bridge:event <?= $singular ?>`.
|
||||||
|
// Listens for `app://event/<?= $event_topic ?>` envelopes published by
|
||||||
|
// <?= $subscriber_short ?> and re-emits them as a typed QML signal.
|
||||||
|
//
|
||||||
|
// Drop into a parent component and connect:
|
||||||
|
//
|
||||||
|
// <?= $singular ?>EventHandler {
|
||||||
|
// on<?= $singular ?>: function(payload) { console.log("hi", payload) }
|
||||||
|
// }
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import PhpQml.Bridge
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: handler
|
||||||
|
|
||||||
|
/** Emitted when the bridge publishes app://event/<?= $event_topic ?>. */
|
||||||
|
signal <?= $signal_name ?>(var payload)
|
||||||
|
|
||||||
|
MercureClient {
|
||||||
|
baseUrl: BackendConnection.url
|
||||||
|
token: BackendConnection.token
|
||||||
|
topics: ["app://event/<?= $event_topic ?>"]
|
||||||
|
|
||||||
|
onUpdate: function(topic, envelope) {
|
||||||
|
if (topic === "app://event/<?= $event_topic ?>") {
|
||||||
|
handler.<?= $signal_name ?>(envelope.data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
39
framework/php/src/Maker/templates/EventSubscriber.tpl.php
Normal file
39
framework/php/src/Maker/templates/EventSubscriber.tpl.php
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
<?= "<?php\n" ?>
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\EventSubscriber;
|
||||||
|
|
||||||
|
use App\Event\<?= $event_short ?>;
|
||||||
|
use PhpQml\Bridge\PublisherInterface;
|
||||||
|
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Republishes <?= $event_short ?> on `app://event/<?= $event_topic ?>`.
|
||||||
|
* Auto-generated alongside the event class — wire `payload` to whatever
|
||||||
|
* shape you want QML clients to receive in the envelope's `data` field.
|
||||||
|
*/
|
||||||
|
final readonly class <?= $subscriber_short ?>
|
||||||
|
|
||||||
|
implements EventSubscriberInterface
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private PublisherInterface $publisher,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getSubscribedEvents(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
<?= $event_short ?>::class => '<?= $handler_method ?>',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function <?= $handler_method ?>(<?= $event_short ?> $event): void
|
||||||
|
{
|
||||||
|
$this->publisher->publish('app://event/<?= $event_topic ?>', [
|
||||||
|
'op' => 'event',
|
||||||
|
'data' => $event->payload,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
21
framework/php/tests/snapshot/TodoCompletedEvent.php
Normal file
21
framework/php/tests/snapshot/TodoCompletedEvent.php
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Event;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Domain event published on `app://event/todo-completed` by
|
||||||
|
* TodoCompletedSubscriber.
|
||||||
|
*
|
||||||
|
* Auto-generated stub — replace the `payload` field with typed
|
||||||
|
* properties matching the event you actually fire.
|
||||||
|
*/
|
||||||
|
final readonly class TodoCompletedEvent
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
/** @var array<string, mixed> */
|
||||||
|
public array $payload = [],
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
31
framework/php/tests/snapshot/TodoCompletedEventHandler.qml
Normal file
31
framework/php/tests/snapshot/TodoCompletedEventHandler.qml
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
// Auto-generated by `bin/console make:bridge:event TodoCompleted`.
|
||||||
|
// Listens for `app://event/todo-completed` envelopes published by
|
||||||
|
// TodoCompletedSubscriber and re-emits them as a typed QML signal.
|
||||||
|
//
|
||||||
|
// Drop into a parent component and connect:
|
||||||
|
//
|
||||||
|
// TodoCompletedEventHandler {
|
||||||
|
// onTodoCompleted: function(payload) { console.log("hi", payload) }
|
||||||
|
// }
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import PhpQml.Bridge
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: handler
|
||||||
|
|
||||||
|
/** Emitted when the bridge publishes app://event/todo-completed. */
|
||||||
|
signal todoCompleted(var payload)
|
||||||
|
|
||||||
|
MercureClient {
|
||||||
|
baseUrl: BackendConnection.url
|
||||||
|
token: BackendConnection.token
|
||||||
|
topics: ["app://event/todo-completed"]
|
||||||
|
|
||||||
|
onUpdate: function(topic, envelope) {
|
||||||
|
if (topic === "app://event/todo-completed") {
|
||||||
|
handler.todoCompleted(envelope.data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
37
framework/php/tests/snapshot/TodoCompletedSubscriber.php
Normal file
37
framework/php/tests/snapshot/TodoCompletedSubscriber.php
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\EventSubscriber;
|
||||||
|
|
||||||
|
use App\Event\TodoCompletedEvent;
|
||||||
|
use PhpQml\Bridge\PublisherInterface;
|
||||||
|
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Republishes TodoCompletedEvent on `app://event/todo-completed`.
|
||||||
|
* Auto-generated alongside the event class — wire `payload` to whatever
|
||||||
|
* shape you want QML clients to receive in the envelope's `data` field.
|
||||||
|
*/
|
||||||
|
final readonly class TodoCompletedSubscriber implements EventSubscriberInterface
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private PublisherInterface $publisher,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getSubscribedEvents(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
TodoCompletedEvent::class => 'onTodoCompleted',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function onTodoCompleted(TodoCompletedEvent $event): void
|
||||||
|
{
|
||||||
|
$this->publisher->publish('app://event/todo-completed', [
|
||||||
|
'op' => 'event',
|
||||||
|
'data' => $event->payload,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -50,13 +50,17 @@ clear_outputs
|
|||||||
( cd "$APP/symfony" \
|
( cd "$APP/symfony" \
|
||||||
&& bin/console make:bridge:resource Todo --no-interaction >/dev/null \
|
&& bin/console make:bridge:resource Todo --no-interaction >/dev/null \
|
||||||
&& bin/console make:bridge:command MarkAllDone --no-interaction >/dev/null \
|
&& bin/console make:bridge:command MarkAllDone --no-interaction >/dev/null \
|
||||||
&& bin/console make:bridge:window Todo --no-interaction >/dev/null )
|
&& bin/console make:bridge:window Todo --no-interaction >/dev/null \
|
||||||
|
&& bin/console make:bridge:event TodoCompleted --no-interaction >/dev/null )
|
||||||
|
|
||||||
check "$APP/symfony/src/Entity/Todo.php" "$SCRIPT_DIR/Todo.php"
|
check "$APP/symfony/src/Entity/Todo.php" "$SCRIPT_DIR/Todo.php"
|
||||||
check "$APP/symfony/src/Controller/TodoController.php" "$SCRIPT_DIR/TodoController.php"
|
check "$APP/symfony/src/Controller/TodoController.php" "$SCRIPT_DIR/TodoController.php"
|
||||||
check "$APP/qml/TodoList.qml" "$SCRIPT_DIR/TodoList.qml"
|
check "$APP/qml/TodoList.qml" "$SCRIPT_DIR/TodoList.qml"
|
||||||
check "$APP/symfony/src/Controller/MarkAllDoneController.php" "$SCRIPT_DIR/MarkAllDoneController.php"
|
check "$APP/symfony/src/Controller/MarkAllDoneController.php" "$SCRIPT_DIR/MarkAllDoneController.php"
|
||||||
check "$APP/qml/TodoWindow.qml" "$SCRIPT_DIR/TodoWindow.qml"
|
check "$APP/qml/TodoWindow.qml" "$SCRIPT_DIR/TodoWindow.qml"
|
||||||
|
check "$APP/symfony/src/Event/TodoCompletedEvent.php" "$SCRIPT_DIR/TodoCompletedEvent.php"
|
||||||
|
check "$APP/symfony/src/EventSubscriber/TodoCompletedSubscriber.php" "$SCRIPT_DIR/TodoCompletedSubscriber.php"
|
||||||
|
check "$APP/qml/TodoCompletedEventHandler.qml" "$SCRIPT_DIR/TodoCompletedEventHandler.qml"
|
||||||
|
|
||||||
# ── Mode 2: --with-dto (re-runs make:bridge:resource only) ────────────
|
# ── Mode 2: --with-dto (re-runs make:bridge:resource only) ────────────
|
||||||
# The entity + QML output is byte-identical between modes; only the
|
# The entity + QML output is byte-identical between modes; only the
|
||||||
|
|||||||
Reference in New Issue
Block a user