Skip to content

Commit fdb7d89

Browse files
committed
refactor: improve error handling and type consistency in List and Reader controllers
1 parent 205a350 commit fdb7d89

15 files changed

Lines changed: 112 additions & 51 deletions

.audit/260719012-combined/10-architektur.md

Lines changed: 50 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -61,46 +61,60 @@ Doku-Drift gegen den aktuellen Code: `ListBuilderFactory` heißt `ListSpecBuilde
6161
>
6262
> **Nutzer-Antwort: Angeglichen -- method: __METHOD__, source, wenn verfügbar: table.id -- übertragen auf gesamte Codebase**
6363
64-
## A-09: `symfony/event-dispatcher` nicht direkt deklariert — Minor (claude, reduzierter Umfang)
65-
66-
`FilterFormFactory` instanziiert direkt `new EventDispatcher()` (`src/Filter/Factory/FilterFormFactory.php:17,70`), deklariert ist aber nur `symfony/event-dispatcher-contracts` (`composer.json:17`); das konkrete Paket kommt nur transitiv über `contao/core-bundle`.
67-
68-
## A-10: `ValidationLoader::executeQuery()` liefert `[]` statt `null` bei abgebrochenem Query-Aufbau — Minor (claude)
69-
70-
Bei `!$qb` wird `[]` zurückgegeben — harmlos (falsy), aber semantisch schief gegenüber dem `?array`-Vertrag, in dem `null` „nicht gefunden" bedeutet (`:117`: `return $entry ?: null;`).
71-
72-
- `src/Engine/Loader/ValidationLoader.php:107-109`
73-
74-
## A-11: Query-Assemblierung lebt in Event-Listener-Prioritäten ohne zentrale Übersicht — Info (claude)
75-
76-
Select@490, Conditions@470, Page@430, Order@420, Join@-450; Integrations-Listener dazwischen (250/220/200/190/100). Die Gesamtordnung ist nirgends zentral dokumentiert (kein Pipeline-Kommentar im `ListQueryDirector`).
77-
78-
- `src/EventListener/QueryStructModifier/SelectModifierListener.php:13`, `ConditionsModifierListener.php:11`, `PageModifierListener.php:11`, `OrderModifierListener.php:12`, `JoinModifierListener.php:10` · `src/Integration/ContaoCalendar/EventListener/CountEventsModifierListener.php:14` u. a.
79-
80-
## A-12: `ViewInterface` ist leerer Marker; Aufrufer müssen downcasten — Info (claude)
81-
82-
Das Interface ist leer (`src/Engine/View/ViewInterface.php:7-9`); `ReaderController` downcastet auf `ValidationView` (`src/Controller/ContentElement/ReaderController.php:127`). Die `@template`-Annotationen sind nur mit dem `generics.noParent`-Ignore in PHPStan haltbar.
83-
84-
## A-13: `#[TaggedIterator]` ist seit Symfony 7.1 deprecated — Info (claude)
85-
86-
Genutzt in drei Registries; relevant für Deprecation-Logs bei Support-Matrix ^5.4|^6|^7. Nachfolger `AutowireIterator` existiert erst ab 6.3 → für die Matrix ggf. `!tagged_iterator` in YAML.
87-
88-
- `src/Registry/EngineModRegistry.php:15` · `src/Registry/ProjectorRegistry.php:19` · `src/Registry/FilterTypeRegistry.php:18`
89-
90-
## A-14: Statische Contao-Aufrufe in Context-DTOs — Info (claude, reduzierter Umfang)
91-
92-
`PageModel::findByPk` in wertartigen Context-Objekten — DB-Zugriffe, testfeindlich, aber Contao-idiomatisch.
64+
> ## A-09: `symfony/event-dispatcher` nicht direkt deklariert — Minor (claude, reduzierter Umfang)
65+
>
66+
> `FilterFormFactory` instanziiert direkt `new EventDispatcher()` (`src/Filter/Factory/FilterFormFactory.php:17,70`), deklariert ist aber nur `symfony/event-dispatcher-contracts` (`composer.json:17`); das konkrete Paket kommt nur transitiv über `contao/core-bundle`.
67+
>
68+
> **Nutzer-Antwort: Required in composer.json**
9369
94-
- `src/Engine/Context/ReaderUrlConfigCreatorTrait.php:18` · `src/Engine/Context/ValidationContext.php:44`
70+
> ## A-10: `ValidationLoader::executeQuery()` liefert `[]` statt `null` bei abgebrochenem Query-Aufbau — Minor (claude)
71+
>
72+
> Bei `!$qb` wird `[]` zurückgegeben — harmlos (falsy), aber semantisch schief gegenüber dem `?array`-Vertrag, in dem `null` „nicht gefunden" bedeutet (`:117`: `return $entry ?: null;`).
73+
>
74+
> - `src/Engine/Loader/ValidationLoader.php:107-109`
75+
>
76+
> **Nutzer-Antwort: Return-type auf `array` angepasst.**
9577
96-
## A-15: Backend-Responses ohne Null-Check auf `$listModel` — Info (claude)
78+
> ## A-11: Query-Assemblierung lebt in Event-Listener-Prioritäten ohne zentrale Übersicht — Info (claude)
79+
>
80+
> Select@490, Conditions@470, Page@430, Order@420, Join@-450; Integrations-Listener dazwischen (250/220/200/190/100). Die Gesamtordnung ist nirgends zentral dokumentiert (kein Pipeline-Kommentar im `ListQueryDirector`).
81+
>
82+
> - `src/EventListener/QueryStructModifier/SelectModifierListener.php:13`, `ConditionsModifierListener.php:11`, `PageModifierListener.php:11`, `OrderModifierListener.php:12`, `JoinModifierListener.php:10` · `src/Integration/ContaoCalendar/EventListener/CountEventsModifierListener.php:14` u. a.
83+
>
84+
> **Nutzer-Antwort: Das muss in einem zukünftigen PR nochmal überarbeitet werden.**
9785
98-
`getRelated()` kann `null` liefern; der Catch deckt nur Exceptions ab. Danach werden `$listModel->title` / `trans($listModel->type)` ungeprüft dereferenziert — in beiden Controllern. (Gelöschte/fehlende Liste → Backend-Crash; siehe auch SEC-03 in [30-sicherheit.md](30-sicherheit.md).)
86+
> ## A-12: `ViewInterface` ist leerer Marker; Aufrufer müssen downcasten — Info (claude)
87+
>
88+
> Das Interface ist leer (`src/Engine/View/ViewInterface.php:7-9`); `ReaderController` downcastet auf `ValidationView` (`src/Controller/ContentElement/ReaderController.php:127`). Die `@template`-Annotationen sind nur mit dem `generics.noParent`-Ignore in PHPStan haltbar.
9989
100-
- `src/Controller/ContentElement/ReaderController.php:220-236` (Zugriff `:232-233`) · `src/Controller/ContentElement/ListViewController.php:154-168` (Zugriff `:166-167`)
90+
> ## A-13: `#[TaggedIterator]` ist seit Symfony 7.1 deprecated — Info (claude)
91+
>
92+
> Genutzt in drei Registries; relevant für Deprecation-Logs bei Support-Matrix ^5.4|^6|^7. Nachfolger `AutowireIterator` existiert erst ab 6.3 → für die Matrix ggf. `!tagged_iterator` in YAML.
93+
>
94+
> - `src/Registry/EngineModRegistry.php:15` · `src/Registry/ProjectorRegistry.php:19` · `src/Registry/FilterTypeRegistry.php:18`
95+
>
96+
> **Nutzer-Antwort: Passt so.**
10197
102-
## A-16: `Engine`-Mods-API mischt Semantiken — Info (claude)
98+
> ## A-14: Statische Contao-Aufrufe in Context-DTOs — Info (claude, reduzierter Umfang)
99+
>
100+
> `PageModel::findByPk` in wertartigen Context-Objekten — DB-Zugriffe, testfeindlich, aber Contao-idiomatisch.
101+
>
102+
> - `src/Engine/Context/ReaderUrlConfigCreatorTrait.php:18` · `src/Engine/Context/ValidationContext.php:44`
103+
>
104+
> **Nutzer-Antwort: Weiterhin statische Aufrufe, aber nun besser gekapselt.**
103105
104-
`addMod()` appendet numerisch, `setMod()`/`unsetMod()` arbeiten mit String-Keys im selben Array; `unsetMod()` kann appendete Mods nicht adressieren — öffentlicher `@api`-Punkt.
106+
> ## A-15: Backend-Responses ohne Null-Check auf `$listModel` — Info (claude)
107+
>
108+
> `getRelated()` kann `null` liefern; der Catch deckt nur Exceptions ab. Danach werden `$listModel->title` / `trans($listModel->type)` ungeprüft dereferenziert — in beiden Controllern. (Gelöschte/fehlende Liste → Backend-Crash; siehe auch SEC-03 in [30-sicherheit.md](30-sicherheit.md).)
109+
>
110+
> - `src/Controller/ContentElement/ReaderController.php:220-236` (Zugriff `:232-233`) · `src/Controller/ContentElement/ListViewController.php:154-168` (Zugriff `:166-167`)
111+
>
112+
> **Nutzer-Antwort: Good Catch! Ist jetzt mit einer entsprechenden Warnung gesichert.**
105113
106-
- `src/Engine/Engine.php:66-93`
114+
> ## A-16: `Engine`-Mods-API mischt Semantiken — Info (claude)
115+
>
116+
> `addMod()` appendet numerisch, `setMod()`/`unsetMod()` arbeiten mit String-Keys im selben Array; `unsetMod()` kann appendete Mods nicht adressieren — öffentlicher `@api`-Punkt.
117+
>
118+
> - `src/Engine/Engine.php:66-93`
119+
>
120+
> **Nutzer-Antwort: Das ist kein Fehler sondern explizit so gewollt. Der Nutzer hat die Wahl, Filter für mehrfache veränderung überschreibbar zu machen, oder nicht. In den meisten Fällen wird das nicht gebraucht, daher reicht Listenindexierung ohne Möglichkeit zur Änderung.**

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"psr/log": "^1.0 || ^2.0 || ^3.0",
1515
"symfony/config": "^5.4 || ^6.0 || ^7.0",
1616
"symfony/dependency-injection": "^5.4 || ^6.0 || ^7.0",
17+
"symfony/event-dispatcher": "^5.4 || ^6.0 || ^7.0",
1718
"symfony/event-dispatcher-contracts": "^1.0 || ^2.0 || ^3.0",
1819
"symfony/filesystem": "^5.4 || ^6.0 || ^7.0",
1920
"symfony/form": "^5.4 || ^6.0 || ^7.0",
@@ -35,7 +36,6 @@
3536
"heimrichhannot/contao-test-utilities-bundle": "^0.1",
3637
"phpunit/phpunit": "^8.0 || ^9.0",
3738
"php-coveralls/php-coveralls": "^2.0",
38-
"symfony/event-dispatcher": "^5.4 || ^6.0 || ^7.0",
3939
"symfony/phpunit-bridge": "^5.4 || ^6.0 || ^7.0",
4040
"phpstan/phpstan": "^1.10",
4141
"phpstan/phpstan-symfony": "^1.2"

src/Controller/ContentElement/ListViewController.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,14 @@ protected function getBackendResponse(Template $template, ContentModel $model, R
215215
return new Response($e->getMessage());
216216
}
217217

218+
if (!$listModel instanceof ListModel) {
219+
return new Response(\sprintf(
220+
'<div><strong class="tl_red">%s</strong></div><div>%s</div>',
221+
$this->translator->trans('reader.invalid_list', [], 'flare'),
222+
Str::formatHeadline($model->headline, withTags: true),
223+
));
224+
}
225+
218226
return new Response(\sprintf(
219227
'<div>%s</div><span>%s</span> <span class="tl_gray">[%s, %s]</span>',
220228
(string) Str::formatHeadline($model->headline),

src/Controller/ContentElement/ReaderController.php

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ protected function getFrontendResponse(Template $template, ContentModel $content
125125
$validationView = $engine->createView();
126126

127127
if (!$validationView instanceof ValidationView) {
128-
throw ViewException::create(ValidationView::class, $validationView, __METHOD__);
128+
throw ViewException::create(ValidationView::class, $validationView, method: __METHOD__);
129129
}
130130

131131
if (!$autoItemModel = $validationView->getModelByAutoItem($autoItem)) {
@@ -226,6 +226,14 @@ protected function getBackendResponse(Template $template, ContentModel $model, R
226226
return new Response($e->getMessage());
227227
}
228228

229+
if (!$listModel instanceof ListModel) {
230+
return new Response(\sprintf(
231+
'<div><strong class="tl_red">%s</strong></div><div>%s</div>',
232+
$this->translator->trans('reader.invalid_list', [], 'flare'),
233+
Str::formatHeadline($model->headline, withTags: true),
234+
));
235+
}
236+
229237
return new Response(\sprintf(
230238
'%s%s <span class="tl_gray">[%s, %s]</span>',
231239
Str::formatHeadline($model->headline, withTags: true),

src/Engine/Context/Factory/InteractiveContextFactory.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,8 @@ public function createFromContent(ContentModel $contentModel, ListSpec $list): I
3838

3939
$config = new InteractiveContext(
4040
paginatorConfig: $paginatorConfig,
41-
sortOrderSequence: $sortOrderSequence,
4241
formName: $filterFormName,
42+
sortOrderSequence: $sortOrderSequence,
4343
contentModelId: (int) $contentModel->id,
4444
formActionPage: (int) $contentModel->{ContentContainer::FIELD_JUMP_TO},
4545
jumpToReaderPageId: $jumpToReaderPageId,

src/Engine/Context/Factory/ValidationContextFactory.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
use Contao\ContentModel;
88
use HeimrichHannot\FlareBundle\DataContainer\ContentContainer;
99
use HeimrichHannot\FlareBundle\Engine\Context\ValidationContext;
10-
use HeimrichHannot\FlareBundle\Engine\View\InteractiveView;
1110
use HeimrichHannot\FlareBundle\List\ListSpec;
1211
use Symfony\Component\Validator\Exception\ValidationFailedException;
1312
use Symfony\Component\Validator\Validator\ValidatorInterface;

src/Engine/Context/InteractiveContext.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,16 @@ public static function getContextType(): string
2323

2424
public function __construct(
2525
public PaginatorConfig $paginatorConfig,
26-
public ?SortOrderSequence $sortOrderSequence = null,
2726
#[Assert\NotBlank] public string $formName,
27+
public ?SortOrderSequence $sortOrderSequence = null,
2828
#[Assert\PositiveOrZero] public int $contentModelId = 0,
2929
#[Assert\PositiveOrZero] public int $formActionPage = 0,
3030
#[Assert\PositiveOrZero] public int $jumpToReaderPageId = 0,
3131
#[Assert\NotBlank] public string $autoItemField = 'id',
3232
public ?string $pageParam = null,
33-
) {}
33+
) {
34+
$this->initJumpToReaderPage();
35+
}
3436

3537
public function getFormName(): string
3638
{

src/Engine/Context/ReaderUrlConfigCreatorTrait.php

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,28 @@
99

1010
trait ReaderUrlConfigCreatorTrait
1111
{
12-
public function createReaderUrlConfig(): ?ReaderUrlConfig
12+
private \Closure $jumpToReaderPage;
13+
14+
final protected function initJumpToReaderPage(): void
1315
{
14-
if (!$this->jumpToReaderPageId) {
15-
return null;
16-
}
16+
$this->jumpToReaderPage = function (): ?PageModel {
17+
$pageModel = PageModel::findByPk($this->jumpToReaderPageId);
18+
$this->jumpToReaderPage = static fn (): ?PageModel => $pageModel;
19+
return $pageModel;
20+
};
21+
}
1722

18-
if (!$pageModel = PageModel::findByPk($this->jumpToReaderPageId)) {
23+
protected function getJumpToReaderPage(): ?PageModel
24+
{
25+
return ($this->jumpToReaderPage)();
26+
}
27+
28+
public function createReaderUrlConfig(): ?ReaderUrlConfig
29+
{
30+
if (!$pageModel = $this->getJumpToReaderPage()) {
1931
return null;
2032
}
2133

2234
return new ReaderUrlConfig(readerPage: $pageModel, autoItemField: $this->autoItemField);
2335
}
24-
}
36+
}

src/Engine/Context/ValidationContext.php

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
use ReaderUrlConfigCreatorTrait;
1717

1818
private PaginatorConfig $paginatorConfig;
19+
private \Closure $jumpToListViewPage;
20+
private \Closure $jumpToReaderPage;
1921

2022
public static function getContextType(): string
2123
{
@@ -29,6 +31,14 @@ public function __construct(
2931
private array $filterValues = [],
3032
) {
3133
$this->paginatorConfig = new PaginatorConfig(itemsPerPage: 1);
34+
35+
$this->jumpToListViewPage = function (): ?PageModel {
36+
$pageModel = PageModel::findByPk($this->jumpToListViewPageId);
37+
$this->jumpToListViewPage = static fn (): ?PageModel => $pageModel;
38+
return $pageModel;
39+
};
40+
41+
$this->initJumpToReaderPage();
3242
}
3343

3444
public function createBackLink(): ?BackLink
@@ -37,7 +47,7 @@ public function createBackLink(): ?BackLink
3747
return null;
3848
}
3949

40-
if (!$pageModel = PageModel::findByPk($this->jumpToListViewPageId)) {
50+
if (!$pageModel = ($this->jumpToListViewPage)()) {
4151
return null;
4252
}
4353

src/Engine/Loader/ValidationLoader.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ public function fetchEntryByAutoItem(string $autoItem): ?array
127127
/**
128128
* @throws \Exception
129129
*/
130-
private function executeQuery(ListSpec $list, ValidationContext $context): ?array
130+
private function executeQuery(ListSpec $list, ValidationContext $context): array
131131
{
132132
$qb = $this->listQueryDirector->createQueryBuilder(new ListQueryConfig(
133133
list: $list,
@@ -145,6 +145,6 @@ private function executeQuery(ListSpec $list, ValidationContext $context): ?arra
145145

146146
$result->free();
147147

148-
return $entry ?: null;
148+
return $entry ?: [];
149149
}
150150
}

0 commit comments

Comments
 (0)