Phase 3 sub-commit 5: maker-output snapshot test + phase closure
framework/php/tests/snapshot/ holds reference output for every shipped
maker (resource Todo, command MarkAllDone, window Todo). The
run.sh script:
- git-archives the skeleton into a temp dir
- composer-installs against the bundle's real path
- removes the existing maker outputs so the regenerators don't bail
- runs the three makers
- diffs each generated file against the matching baseline
CI / make quality fail on any drift; if a template change is intended,
the baselines must be regenerated in the same commit. Wired into:
- framework/skeleton/Makefile's `quality` target (local/dev runs)
- .gitea/workflows/ci.yml (CI runs after qmllint)
Plus a few hardenings discovered while wiring this up:
- The resource maker template now injects NormalizerInterface
(not SerializerInterface — that interface lacks ::normalize()).
All Todo controllers re-rendered to match.
- The command maker template emits a $this->em->flush() so the
injected EntityManager isn't a property.onlyWritten violation
in PHPStan after the user fills in the body.
- phpstan.neon and php-cs-fixer's Finder both exclude tests/snapshot
so the baselines aren't auto-rewritten or analysed as live code.
CI workflow now also installs FrankenPHP, builds the todo example, and
runs the bridge-integration test from Phase 3 sub-commit 4.
Phase 3 done. Outstanding follow-ups (deferred per spec): the
qmltestrunner-driven QML unit tests, make:bridge:event,
make:bridge:read-model, ReactiveObject pagination.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -67,3 +67,22 @@ jobs:
|
|||||||
- name: qmllint
|
- name: qmllint
|
||||||
working-directory: framework/skeleton
|
working-directory: framework/skeleton
|
||||||
run: cmake --build build/qml --target all_qmllint
|
run: cmake --build build/qml --target all_qmllint
|
||||||
|
|
||||||
|
- name: Install FrankenPHP
|
||||||
|
run: |
|
||||||
|
curl -fsSL -o /usr/local/bin/frankenphp \
|
||||||
|
https://github.com/php/frankenphp/releases/download/v1.12.2/frankenphp-linux-x86_64
|
||||||
|
chmod +x /usr/local/bin/frankenphp
|
||||||
|
|
||||||
|
- name: Maker-output snapshot test
|
||||||
|
run: framework/php/tests/snapshot/run.sh
|
||||||
|
|
||||||
|
- name: Build the todo example
|
||||||
|
working-directory: examples/todo
|
||||||
|
run: |
|
||||||
|
make install
|
||||||
|
make build
|
||||||
|
|
||||||
|
- name: Bridge-integration test (HTTP/SSE round-trip + crash-recover)
|
||||||
|
working-directory: examples/todo
|
||||||
|
run: ./tests/integration.sh
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ use Symfony\Component\HttpFoundation\JsonResponse;
|
|||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use Symfony\Component\Routing\Attribute\Route;
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
use Symfony\Component\Serializer\SerializerInterface;
|
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Auto-generated CRUD controller for the Todo bridge resource.
|
* Auto-generated CRUD controller for the Todo bridge resource.
|
||||||
@@ -21,7 +21,7 @@ final class TodoController
|
|||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly EntityManagerInterface $em,
|
private readonly EntityManagerInterface $em,
|
||||||
private readonly SerializerInterface $serializer,
|
private readonly NormalizerInterface $normalizer,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,7 +30,7 @@ final class TodoController
|
|||||||
{
|
{
|
||||||
$items = $this->em->getRepository(Todo::class)->findAll();
|
$items = $this->em->getRepository(Todo::class)->findAll();
|
||||||
|
|
||||||
return new JsonResponse($this->serializer->normalize($items, 'json'));
|
return new JsonResponse($this->normalizer->normalize($items, 'json'));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Route('', name: 'todo_create', methods: ['POST'])]
|
#[Route('', name: 'todo_create', methods: ['POST'])]
|
||||||
@@ -51,7 +51,7 @@ final class TodoController
|
|||||||
$this->em->flush();
|
$this->em->flush();
|
||||||
|
|
||||||
return new JsonResponse(
|
return new JsonResponse(
|
||||||
$this->serializer->normalize($entity, 'json'),
|
$this->normalizer->normalize($entity, 'json'),
|
||||||
Response::HTTP_CREATED,
|
Response::HTTP_CREATED,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -81,7 +81,7 @@ final class TodoController
|
|||||||
|
|
||||||
$this->em->flush();
|
$this->em->flush();
|
||||||
|
|
||||||
return new JsonResponse($this->serializer->normalize($entity, 'json'));
|
return new JsonResponse($this->normalizer->normalize($entity, 'json'));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Route('/{id}', name: 'todo_delete', methods: ['DELETE'])]
|
#[Route('/{id}', name: 'todo_delete', methods: ['DELETE'])]
|
||||||
|
|||||||
@@ -4,7 +4,12 @@ $finder = (new PhpCsFixer\Finder())
|
|||||||
->in([__DIR__ . '/src', __DIR__ . '/tests'])
|
->in([__DIR__ . '/src', __DIR__ . '/tests'])
|
||||||
// Maker templates use short-echo syntax and alternative-syntax control
|
// Maker templates use short-echo syntax and alternative-syntax control
|
||||||
// structures by design — cs-fixer's @Symfony rules would mangle them.
|
// structures by design — cs-fixer's @Symfony rules would mangle them.
|
||||||
->notPath('Maker/templates');
|
->notPath('Maker/templates')
|
||||||
|
// Maker-output snapshot baselines are the makers' exact output and
|
||||||
|
// must not be auto-rewritten; if they need updating, the maker
|
||||||
|
// template changes and the snapshot is regenerated explicitly.
|
||||||
|
// (notPath is relative to each `in()` dir, not the project root.)
|
||||||
|
->notPath('snapshot');
|
||||||
|
|
||||||
return (new PhpCsFixer\Config())
|
return (new PhpCsFixer\Config())
|
||||||
->setRiskyAllowed(true)
|
->setRiskyAllowed(true)
|
||||||
|
|||||||
@@ -12,3 +12,5 @@ parameters:
|
|||||||
# Maker templates use variables injected by the Generator at
|
# Maker templates use variables injected by the Generator at
|
||||||
# render time; static analysis can't see the binding.
|
# render time; static analysis can't see the binding.
|
||||||
- src/Maker/templates/*
|
- src/Maker/templates/*
|
||||||
|
# Snapshot baselines are reference output, not maintained code.
|
||||||
|
- tests/snapshot/*
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ use Symfony\Component\Console\Input\InputOption;
|
|||||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
use Symfony\Component\Routing\Attribute\Route;
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
use Symfony\Component\Serializer\SerializerInterface;
|
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
||||||
use Symfony\Component\Uid\Uuid;
|
use Symfony\Component\Uid\Uuid;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -163,7 +163,7 @@ final class BridgeResourceMaker extends AbstractMaker
|
|||||||
$dependencies->addClassDependency(JsonResponse::class, 'symfony/http-foundation');
|
$dependencies->addClassDependency(JsonResponse::class, 'symfony/http-foundation');
|
||||||
$dependencies->addClassDependency(Request::class, 'symfony/http-foundation');
|
$dependencies->addClassDependency(Request::class, 'symfony/http-foundation');
|
||||||
$dependencies->addClassDependency(EntityManagerInterface::class, 'doctrine/orm');
|
$dependencies->addClassDependency(EntityManagerInterface::class, 'doctrine/orm');
|
||||||
$dependencies->addClassDependency(SerializerInterface::class, 'symfony/serializer');
|
$dependencies->addClassDependency(NormalizerInterface::class, 'symfony/serializer');
|
||||||
$dependencies->addClassDependency(Uuid::class, 'symfony/uid');
|
$dependencies->addClassDependency(Uuid::class, 'symfony/uid');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,9 @@ final class <?= $controller_short ?>
|
|||||||
{
|
{
|
||||||
// TODO: implement <?= $singular ?>.
|
// TODO: implement <?= $singular ?>.
|
||||||
// Read inputs from $request->getPayload() / json_decode($request->getContent(), true).
|
// Read inputs from $request->getPayload() / json_decode($request->getContent(), true).
|
||||||
// Mutate entities, then $this->em->flush().
|
// Mutate entities, then $this->em->flush() (already wired up below).
|
||||||
|
|
||||||
|
$this->em->flush();
|
||||||
|
|
||||||
return new JsonResponse(['ok' => true]);
|
return new JsonResponse(['ok' => true]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ use Symfony\Component\HttpFoundation\JsonResponse;
|
|||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use Symfony\Component\Routing\Attribute\Route;
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
use Symfony\Component\Serializer\SerializerInterface;
|
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Auto-generated CRUD controller for the <?= $singular ?> bridge resource.
|
* Auto-generated CRUD controller for the <?= $singular ?> bridge resource.
|
||||||
@@ -21,7 +21,7 @@ final class <?= $entity_short ?>Controller
|
|||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly EntityManagerInterface $em,
|
private readonly EntityManagerInterface $em,
|
||||||
private readonly SerializerInterface $serializer,
|
private readonly NormalizerInterface $normalizer,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,7 +30,7 @@ final class <?= $entity_short ?>Controller
|
|||||||
{
|
{
|
||||||
$items = $this->em->getRepository(<?= $entity_short ?>::class)->findAll();
|
$items = $this->em->getRepository(<?= $entity_short ?>::class)->findAll();
|
||||||
|
|
||||||
return new JsonResponse($this->serializer->normalize($items, 'json'));
|
return new JsonResponse($this->normalizer->normalize($items, 'json'));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Route('', name: '<?= $resource ?>_create', methods: ['POST'])]
|
#[Route('', name: '<?= $resource ?>_create', methods: ['POST'])]
|
||||||
@@ -49,7 +49,7 @@ final class <?= $entity_short ?>Controller
|
|||||||
$this->em->flush();
|
$this->em->flush();
|
||||||
|
|
||||||
return new JsonResponse(
|
return new JsonResponse(
|
||||||
$this->serializer->normalize($entity, 'json'),
|
$this->normalizer->normalize($entity, 'json'),
|
||||||
Response::HTTP_CREATED,
|
Response::HTTP_CREATED,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -76,7 +76,7 @@ final class <?= $entity_short ?>Controller
|
|||||||
|
|
||||||
$this->em->flush();
|
$this->em->flush();
|
||||||
|
|
||||||
return new JsonResponse($this->serializer->normalize($entity, 'json'));
|
return new JsonResponse($this->normalizer->normalize($entity, 'json'));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Route('/{id}', name: '<?= $resource ?>_delete', methods: ['DELETE'])]
|
#[Route('/{id}', name: '<?= $resource ?>_delete', methods: ['DELETE'])]
|
||||||
|
|||||||
35
framework/php/tests/snapshot/MarkAllDoneController.php
Normal file
35
framework/php/tests/snapshot/MarkAllDoneController.php
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Controller;
|
||||||
|
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-generated domain command endpoint. Fill in the body and persist
|
||||||
|
* via $this->em->flush(). Doctrine entities tagged with #[BridgeResource]
|
||||||
|
* publish their changes to Mercure automatically.
|
||||||
|
*/
|
||||||
|
final class MarkAllDoneController
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly EntityManagerInterface $em,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/api/mark-all-done', name: 'mark-all-done', methods: ['POST'])]
|
||||||
|
public function __invoke(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
// TODO: implement MarkAllDone.
|
||||||
|
// Read inputs from $request->getPayload() / json_decode($request->getContent(), true).
|
||||||
|
// Mutate entities, then $this->em->flush() (already wired up below).
|
||||||
|
|
||||||
|
$this->em->flush();
|
||||||
|
|
||||||
|
return new JsonResponse(['ok' => true]);
|
||||||
|
}
|
||||||
|
}
|
||||||
54
framework/php/tests/snapshot/Todo.php
Normal file
54
framework/php/tests/snapshot/Todo.php
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Entity;
|
||||||
|
|
||||||
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
use PhpQml\Bridge\Attribute\BridgeResource;
|
||||||
|
use Symfony\Component\Uid\Uuid;
|
||||||
|
|
||||||
|
#[ORM\Entity]
|
||||||
|
#[BridgeResource(name: 'todo')]
|
||||||
|
class Todo
|
||||||
|
{
|
||||||
|
#[ORM\Id]
|
||||||
|
#[ORM\Column(type: 'uuid', unique: true)]
|
||||||
|
private Uuid $id;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255)]
|
||||||
|
private string $title = '';
|
||||||
|
|
||||||
|
#[ORM\Column]
|
||||||
|
private bool $done = false;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->id = Uuid::v7();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getId(): Uuid
|
||||||
|
{
|
||||||
|
return $this->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTitle(): string
|
||||||
|
{
|
||||||
|
return $this->title;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setTitle(string $title): void
|
||||||
|
{
|
||||||
|
$this->title = $title;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isDone(): bool
|
||||||
|
{
|
||||||
|
return $this->done;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setDone(bool $done): void
|
||||||
|
{
|
||||||
|
$this->done = $done;
|
||||||
|
}
|
||||||
|
}
|
||||||
101
framework/php/tests/snapshot/TodoController.php
Normal file
101
framework/php/tests/snapshot/TodoController.php
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Controller;
|
||||||
|
|
||||||
|
use App\Entity\Todo;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-generated CRUD controller for the Todo bridge resource.
|
||||||
|
* Edit freely — re-running make:bridge:resource won't overwrite this file.
|
||||||
|
*/
|
||||||
|
#[Route('/api/todos')]
|
||||||
|
final class TodoController
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly EntityManagerInterface $em,
|
||||||
|
private readonly NormalizerInterface $normalizer,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('', name: 'todo_list', methods: ['GET'])]
|
||||||
|
public function list(): JsonResponse
|
||||||
|
{
|
||||||
|
$items = $this->em->getRepository(Todo::class)->findAll();
|
||||||
|
|
||||||
|
return new JsonResponse($this->normalizer->normalize($items, 'json'));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('', name: 'todo_create', methods: ['POST'])]
|
||||||
|
public function create(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$data = json_decode((string) $request->getContent(), true) ?? [];
|
||||||
|
$entity = new Todo();
|
||||||
|
|
||||||
|
if (isset($data['title'])) {
|
||||||
|
$entity->setTitle((string) $data['title']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($data['done'])) {
|
||||||
|
$entity->setDone((bool) $data['done']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->em->persist($entity);
|
||||||
|
$this->em->flush();
|
||||||
|
|
||||||
|
return new JsonResponse(
|
||||||
|
$this->normalizer->normalize($entity, 'json'),
|
||||||
|
Response::HTTP_CREATED,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/{id}', name: 'todo_update', methods: ['PATCH'])]
|
||||||
|
public function update(string $id, Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$entity = $this->em->getRepository(Todo::class)->find($id);
|
||||||
|
|
||||||
|
if (null === $entity) {
|
||||||
|
return new JsonResponse(
|
||||||
|
['title' => 'Not Found', 'status' => 404],
|
||||||
|
Response::HTTP_NOT_FOUND,
|
||||||
|
['Content-Type' => 'application/problem+json'],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = json_decode((string) $request->getContent(), true) ?? [];
|
||||||
|
|
||||||
|
if (isset($data['title'])) {
|
||||||
|
$entity->setTitle((string) $data['title']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($data['done'])) {
|
||||||
|
$entity->setDone((bool) $data['done']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->em->flush();
|
||||||
|
|
||||||
|
return new JsonResponse($this->normalizer->normalize($entity, 'json'));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/{id}', name: 'todo_delete', methods: ['DELETE'])]
|
||||||
|
public function delete(string $id): JsonResponse
|
||||||
|
{
|
||||||
|
$entity = $this->em->getRepository(Todo::class)->find($id);
|
||||||
|
|
||||||
|
if (null === $entity) {
|
||||||
|
return new JsonResponse(null, Response::HTTP_NO_CONTENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->em->remove($entity);
|
||||||
|
$this->em->flush();
|
||||||
|
|
||||||
|
return new JsonResponse(null, Response::HTTP_NO_CONTENT);
|
||||||
|
}
|
||||||
|
}
|
||||||
28
framework/php/tests/snapshot/TodoList.qml
Normal file
28
framework/php/tests/snapshot/TodoList.qml
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
// Auto-generated by `bin/console make:bridge:resource Todo`.
|
||||||
|
// Drop this into your QML and customize the delegate to taste.
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import PhpQml.Bridge
|
||||||
|
|
||||||
|
ListView {
|
||||||
|
id: todoList
|
||||||
|
|
||||||
|
model: ReactiveListModel {
|
||||||
|
baseUrl: BackendConnection.url
|
||||||
|
token: BackendConnection.token
|
||||||
|
source: "/api/todos"
|
||||||
|
topic: "app://model/todo"
|
||||||
|
}
|
||||||
|
|
||||||
|
delegate: ItemDelegate {
|
||||||
|
required property string id
|
||||||
|
required property string title
|
||||||
|
required property bool done
|
||||||
|
required property bool pending
|
||||||
|
|
||||||
|
text: title + (done ? " ✓" : "")
|
||||||
|
opacity: pending ? 0.5 : 1.0
|
||||||
|
width: ListView.view.width
|
||||||
|
}
|
||||||
|
}
|
||||||
36
framework/php/tests/snapshot/TodoWindow.qml
Normal file
36
framework/php/tests/snapshot/TodoWindow.qml
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
// Auto-generated by `bin/console make:bridge:window Todo`.
|
||||||
|
// Edit freely; re-running the maker won't overwrite this file.
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import QtQuick.Window
|
||||||
|
import PhpQml.Bridge
|
||||||
|
|
||||||
|
ApplicationWindow {
|
||||||
|
id: todoWindow
|
||||||
|
width: 720
|
||||||
|
height: 520
|
||||||
|
visible: true
|
||||||
|
title: "Todo"
|
||||||
|
|
||||||
|
AppShell {
|
||||||
|
anchors.fill: parent
|
||||||
|
|
||||||
|
// Replace this with the window's content. The AppShell parent
|
||||||
|
// takes care of Reconnecting / Offline UI overlays.
|
||||||
|
ColumnLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 16
|
||||||
|
|
||||||
|
Label {
|
||||||
|
text: "Todo"
|
||||||
|
font.pixelSize: 18
|
||||||
|
font.bold: true
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: window content here.
|
||||||
|
Item { Layout.fillWidth: true; Layout.fillHeight: true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
63
framework/php/tests/snapshot/run.sh
Executable file
63
framework/php/tests/snapshot/run.sh
Executable file
@@ -0,0 +1,63 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Maker-output snapshot test.
|
||||||
|
# Runs the three Phase-2/Phase-3 makers in a clean temp app and diffs
|
||||||
|
# the output against the baselines in this directory. Detects silent
|
||||||
|
# generator drift: any change to a maker template requires the
|
||||||
|
# corresponding baseline to be updated in the same commit.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)"
|
||||||
|
BUNDLE="$PROJECT_ROOT/framework/php"
|
||||||
|
WORK="$(mktemp -d)"
|
||||||
|
|
||||||
|
trap 'rm -rf "$WORK"' EXIT INT TERM
|
||||||
|
|
||||||
|
# Mirror the skeleton's tracked files into the temp dir.
|
||||||
|
git -C "$PROJECT_ROOT" archive HEAD framework/skeleton/ | tar -xf - -C "$WORK"
|
||||||
|
APP="$WORK/framework/skeleton"
|
||||||
|
|
||||||
|
# Point the test app's path repo at the actual bundle dir (the
|
||||||
|
# default '../../php' would resolve relative to /tmp).
|
||||||
|
sed -i "s|\"../../php\"|\"$BUNDLE\"|" "$APP/symfony/composer.json"
|
||||||
|
rm -f "$APP/symfony/composer.lock"
|
||||||
|
( cd "$APP/symfony" && composer install --no-interaction --quiet )
|
||||||
|
|
||||||
|
# Remove the existing maker outputs so the regenerators don't bail.
|
||||||
|
rm -f "$APP/symfony/src/Entity/Todo.php"
|
||||||
|
rm -f "$APP/symfony/src/Controller/TodoController.php"
|
||||||
|
rm -f "$APP/qml/TodoList.qml"
|
||||||
|
|
||||||
|
# Run every maker we cover.
|
||||||
|
( cd "$APP/symfony" \
|
||||||
|
&& bin/console make:bridge:resource Todo --no-interaction >/dev/null \
|
||||||
|
&& bin/console make:bridge:command MarkAllDone --no-interaction >/dev/null \
|
||||||
|
&& bin/console make:bridge:window Todo --no-interaction >/dev/null )
|
||||||
|
|
||||||
|
# Compare each generated file to its snapshot baseline.
|
||||||
|
fail=0
|
||||||
|
check() {
|
||||||
|
local generated="$1"
|
||||||
|
local baseline="$2"
|
||||||
|
if ! diff -q "$generated" "$baseline" >/dev/null 2>&1; then
|
||||||
|
echo "✗ snapshot mismatch: $(basename "$baseline")" >&2
|
||||||
|
diff -u "$baseline" "$generated" >&2 || true
|
||||||
|
fail=1
|
||||||
|
else
|
||||||
|
echo "✓ $(basename "$baseline")"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
check "$APP/symfony/src/Entity/Todo.php" "$SCRIPT_DIR/Todo.php"
|
||||||
|
check "$APP/symfony/src/Controller/TodoController.php" "$SCRIPT_DIR/TodoController.php"
|
||||||
|
check "$APP/qml/TodoList.qml" "$SCRIPT_DIR/TodoList.qml"
|
||||||
|
check "$APP/symfony/src/Controller/MarkAllDoneController.php" "$SCRIPT_DIR/MarkAllDoneController.php"
|
||||||
|
check "$APP/qml/TodoWindow.qml" "$SCRIPT_DIR/TodoWindow.qml"
|
||||||
|
|
||||||
|
if [ "$fail" -ne 0 ]; then
|
||||||
|
echo "Snapshot test failed. If the change is intended, update the baselines under $SCRIPT_DIR/." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "All maker outputs match snapshots."
|
||||||
@@ -37,6 +37,7 @@ clean: ## Remove build artefacts
|
|||||||
rm -rf $(BUILD_DIR)
|
rm -rf $(BUILD_DIR)
|
||||||
|
|
||||||
.PHONY: quality
|
.PHONY: quality
|
||||||
quality: build ## Run PHPStan, php-cs-fixer (check), PHPUnit, qmllint
|
quality: build ## Run PHPStan, php-cs-fixer (check), PHPUnit, qmllint, maker snapshots
|
||||||
cd ../php && composer quality
|
cd ../php && composer quality
|
||||||
cmake --build $(BUILD_DIR) --target all_qmllint
|
cmake --build $(BUILD_DIR) --target all_qmllint
|
||||||
|
../php/tests/snapshot/run.sh
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ use Symfony\Component\HttpFoundation\JsonResponse;
|
|||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use Symfony\Component\Routing\Attribute\Route;
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
use Symfony\Component\Serializer\SerializerInterface;
|
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Auto-generated CRUD controller for the Todo bridge resource.
|
* Auto-generated CRUD controller for the Todo bridge resource.
|
||||||
@@ -21,7 +21,7 @@ final class TodoController
|
|||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly EntityManagerInterface $em,
|
private readonly EntityManagerInterface $em,
|
||||||
private readonly SerializerInterface $serializer,
|
private readonly NormalizerInterface $normalizer,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,7 +30,7 @@ final class TodoController
|
|||||||
{
|
{
|
||||||
$items = $this->em->getRepository(Todo::class)->findAll();
|
$items = $this->em->getRepository(Todo::class)->findAll();
|
||||||
|
|
||||||
return new JsonResponse($this->serializer->normalize($items, 'json'));
|
return new JsonResponse($this->normalizer->normalize($items, 'json'));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Route('', name: 'todo_create', methods: ['POST'])]
|
#[Route('', name: 'todo_create', methods: ['POST'])]
|
||||||
@@ -51,7 +51,7 @@ final class TodoController
|
|||||||
$this->em->flush();
|
$this->em->flush();
|
||||||
|
|
||||||
return new JsonResponse(
|
return new JsonResponse(
|
||||||
$this->serializer->normalize($entity, 'json'),
|
$this->normalizer->normalize($entity, 'json'),
|
||||||
Response::HTTP_CREATED,
|
Response::HTTP_CREATED,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -81,7 +81,7 @@ final class TodoController
|
|||||||
|
|
||||||
$this->em->flush();
|
$this->em->flush();
|
||||||
|
|
||||||
return new JsonResponse($this->serializer->normalize($entity, 'json'));
|
return new JsonResponse($this->normalizer->normalize($entity, 'json'));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Route('/{id}', name: 'todo_delete', methods: ['DELETE'])]
|
#[Route('/{id}', name: 'todo_delete', methods: ['DELETE'])]
|
||||||
|
|||||||
Reference in New Issue
Block a user