79 błędów w kodzie Symfony który przeszedł PHPStan
ai,php,statyczna-stronaCzęść 2 z 7
- 368 testów dla statycznego generatora stron — po co?
- 79 błędów w kodzie Symfony który przeszedł PHPStan
- XSS, open redirecty i path traversal na 'statycznej' stronie
- Wzorzec lokalnych nadpisań — szablony Symfony bez forkowania
- Open-source notACMS — pełna lista kontrolna
- Jak notACMS buduje statyczną stronę — pełny pipeline
- Budowanie produkcyjnego motywu notACMS — od szablonu do wydania
To część 2 serii o przygotowaniu notACMS do wydania jako open-source. Część 1 opisuje suite testowy. Oryginalna seria o migracji z WordPressa opisuje samą migrację.
Kod przeszedł PHPStan level 6. PHP CS Fixer nie miał nic do zarzucenia. Rector dry-run czysty. Przejrzałem każdy plik osobiście. Byłem gotowy na open-source.
Potem poprosiłem AI o audyt na podstawie 200-linijkowego pliku instrukcji. Znalazł 79 problemów.
Setup: AGENTS.md jako instrukcja audytu
Nie "przejrzyj mój kod" — to daje generyczne uwagi o obsłudze błędów i przypadkach brzegowych. Zamiast tego szczegółowa checklista w AGENTS.md:
- Zasady architektury — tylko klasy final, segregacja interfejsów, value objects zamiast tablic asocjacyjnych
- Konwencje nazewnicze —
XxxInterface→Xxx,public constw interfejsach - Wzorce bezpieczeństwa — guardy przed path traversal, walidacja przekierowań, sanitizacja danych
- Zasady stylu kodu — warunki Yody, pusta linia przed return, strict types
- Synchronizacja dokumentacji — tabele serwisów zgodne z
src/, tabele zmiennych zgodne z_variables.scss
AI nie zgadywało czego szukać. Wykonywało instrukcje. Różnica jest taka sama jak między przeglądem kodu od kogoś kto zna projekt a przeglądem od kogoś kto go nie zna.
Co AI złapało, a ja przeoczyłem
Mutowalny stan w cache — ContentTree::setIncludeDrafts() mutował obiekt z cache. Ta sama instancja drzewa była współdzielona między requestami, więc przełączenie draftów w jednym requeście wpływało na następny. Race condition czekający na okazję. Rozwiązanie: uczynić ContentTree niemutowalnym i przenieść filtrowanie draftów do warstwy serwisu.
Naruszenia SOLID — SiteConfigServiceInterface miał 16 metod. Zasada projektu to max 5. ContentTree miał 20+ metod robiących strukturę danych, silnik zapytań i scoring rekomendacji — trzy zadania w jednej klasie. Rozwiązanie: wyciągnąć RelatedPostsService (73-linijkowy algorytm scoringu → osobny serwis z własnym interfejsem).
Zduplikowana logika — readingTime() i excerpt() obie robiły strip_tags + str_word_count na tej samej zawartości HTML. Wyciągnięte do wspólnej metody getPlainText().
Luki w dostępności — Zagnieżdżone elementy <label> w formularzu kontaktowym (niepoprawny HTML), brakujące role="alert" na spanach błędów, aria-current="true" zamiast "page". Każde poprawka na jedną linię, ale niewidoczne bez checklisty.
Gnicie dokumentacji — docs/STYLEGUIDE.md podający Bootstrap blue (#0d6efd) zamiast faktycznego zielonego (#2d8a4e). docs/ARCHITECTURE.md odwołujący się do fantomowych plików: tagline.js, contact_widget.html.twig, _header.scss — żaden nie istniał.
Co AI źle oceniło (false positives)
CSRF na formularzu kontaktowym — oznaczone jako luka, ale celowe. Statyczne, pre-renderowane strony nie mogą używać tokenów CSRF powiązanych z sesją — token jest wypalony w HTML w czasie buildu, powiązany z sesją serwera buildowego. Każdy odwiedzający dostaje ten sam token. Turnstile + nagłówek X-Requested-With zapewniają rzeczywistą ochronę.
Dane użytkownika w komunikatach błędów 404 — oznaczone jako ryzyko enumeracji, ale te komunikaty pojawiają się tylko podczas statycznego buildu, nie dla odwiedzających. Produkcyjne strony błędów Symfony nie ujawniają komunikatów wyjątków.
"Niejodyczne" porównania — oznaczone wzorce zmienna-vs-metoda jak $page->directoryKey() === $directoryKey, ale prawdziwa reguła Yody dotyczy literałów (null, false, stringi) po lewej. Porównania zmienna-vs-zmienna nie wymagają odwracania.
Co zmieniłem vs co zaakceptowałem
Około 60 problemów naprawionych. Około 19 zaakceptowanych:
- Zduplikowane serwisy podglądu —
DraftPreviewServiceiScheduledPreviewServicesą prawie identyczne, ale każdy ma 3 linie. Sparametryzowanie wymagałoby ServiceLocatora, wstrzykiwania enuma i customowej serializacji na data collectorze. Więcej złożoności niż oryginał. Zaakceptowane. - Szeroki
SiteConfigServiceInterface— 16 metod, ale to obiekt konfiguracyjny, nie serwis biznesowy. Podział dotyka 20 plików dla porządku anotacji tylko. Zaakceptowane.
Naprawki, które się liczyły:
// Przed: mutowalne drzewo w cache
$tree->setIncludeDrafts(true); // mutuje współdzieloną instancję
// Po: niemutowalne drzewo, filtrowanie w warstwie serwisu
$posts = $this->contentService->getPosts($locale, includeDrafts: true);
// Przed: 73-linijkowy algorytm w ContentTree
public function getRelatedPosts(ContentItem $post, int $limit = 3): array
{
// ... 73 linie scoringu tagów/kategorii/serii
}
// Po: osobny serwis
final class RelatedPostsService implements RelatedPostsServiceInterface
{
public function findRelated(ContentItem $post, ContentTree $tree, int $limit = 3): array
{
// ... ta sama logika, ale izolowana
}
}
Wniosek
"Przejrzyj mój kod" daje generyczne uwagi. "Audytuj według tych 200 linii standardów projektu" daje konkretne, akcjonowalne wyniki. AGENTS.md nie służy tylko do budowania — służy też do przeglądów. AI złapało rzeczy, na które patrzyłem dziesiątki razy, bo sprawdzało według reguł, a nie polegało na znajomości.
W następnym poście opisuję luki bezpieczeństwa wśród tych 79 problemów: XSS w wyszukiwarce, open redirecty i path traversal na "statycznej" stronie.