var_dump dla agenta: agentic coding w Symfony
Jak dać agentowi kodującemu dostęp do profilera, kontenera i logów przez MCP — i czemu to zmienia jakość generowanego kodu bardziej niż lepszy prompt.
Większość zespołów używa agentów kodujących tak: agent czyta pliki, generuje kod, człowiek sprawdza, czy działa. To działa zaskakująco dobrze — i zaskakująco często kończy się pętlą „nie działa, popraw", w której agent zgaduje przyczynę błędu, bo nie ma jak jej zobaczyć.
Tymczasem Symfony jest frameworkiem, który o sobie wie prawie wszystko: skompilowany kontener, profiler z pełnym zapisem każdego requestu, dziesiątki komend debug:* i lint:*. Przez lata budowaliśmy tę introspekcję dla ludzi. Temat właśnie wszedł na główną scenę — na czerwcowym SymfonyOnline jednym z punktów programu jest Symfony Mate, „real runtime context for AI coding assistants". Ten wpis jest o tym, co się za tym hasłem kryje w praktyce: jak podać introspekcję frameworka agentowi — i co się wtedy zmienia.
Problem: agent widzi kod, nie widzi aplikacji
Agent czytający repozytorium widzi to, co widzi nowy programista pierwszego dnia: pliki. Nie widzi, że autowiring rozwiązał interfejs na inną implementację, niż sugeruje nazwa. Nie widzi, że listener z priority: -10 odpala się po tym, który właśnie edytuje. Nie widzi siedemnastu zapytań SQL, które wygenerował jego niewinnie wyglądający diff.
W efekcie agent robi to, w czym jest najlepszy: generuje prawdopodobny kod. Kod, który wygląda jak poprawny kod Symfony. Statystycznie często jest poprawny. Ale gdy nie jest, agent nie ma żadnego mechanizmu, żeby się o tym dowiedzieć — więc halucynuje przyczynę i „poprawia" nie to, co trzeba. Każdy, kto spędził wieczór na takiej pętli, ma prawo być sceptykiem.
Rozwiązanie nie polega na lepszym promptcie. Polega na tym samym, co robimy z juniorem: dać dostęp do narzędzi diagnostycznych i procedury ich użycia.
Podejście pierwsze: dać agentowi konsolę
Najtańszy ruch: pozwolić agentowi wołać bin/console. Zero konfiguracji, a pokrywa zaskakująco dużo:
debug:container,debug:autowiring— co naprawdę jest w kontenerze, a nie co sugerują YAML-e,debug:router,debug:event-dispatcher,debug:config— skompilowana prawda o routingu, listenerach i konfiguracji,lint:container,lint:twig,lint:yaml,doctrine:schema:validate— walidacja bez odpalania przeglądarki.
To już zmienia reguły gry: zamiast zgadywać wynik kompilacji kontenera, agent może go sprawdzić. W Expressie czy Django nie ma odpowiednika debug:container — to jest realna, rzadko zauważana przewaga Symfony w pracy z AI.
Ale CLI ma trzy ograniczenia. Po pierwsze, jest statyczne: mówi, jak aplikacja jest poskładana, a nie co się przed chwilą wydarzyło. Po drugie, output jest dla ludzi — tabelki ASCII, które agent musi parsować, paląc tokeny i czasem się myląc. Po trzecie, dostęp do shella to w praktyce dostęp do wszystkiego; allowlisty komend łata się dziurawo.
Podejście drugie: MCP, czyli Symfony AI Mate
Tu wchodzi MCP — protokół, którym agent (dowolny: Claude Code, Cursor, Copilot, cokolwiek z obsługą MCP) rozmawia z zewnętrznymi narzędziami. Narzędzie deklaruje, co potrafi, opisuje parametry i zwraca ustrukturyzowane dane. Lista narzędzi z opisami jest dla agenta dokumentacją, której nie trzeba pisać.
W ekosystemie Symfony rolę takiego serwera pełni Symfony AI Mate — oficjalne narzędzie deweloperskie z inicjatywy symfony/ai (w chwili pisania: v0.9.0). Sam rdzeń jest celowo mały — to rama z systemem rozszerzeń; toole symfoniowe przychodzą z osobnym pakietem-bridge’em:
composer require --dev symfony/ai-mate symfony/ai-symfony-mate-extension
vendor/bin/mate init
vendor/bin/mate discover
Po wpięciu do konfiguracji agenta (mcp.json, transport STDIO: vendor/bin/mate serve) agent dostaje:
symfony-services,symfony-service-detail— introspekcję kontenera jako dane, nie tabelkę,symfony-profiler-list,symfony-profiler-get— dostęp do profilera: wyjątki ze stack trace, zapytania Doctrine z czasami, odpalone listenery, deprecations — z konkretnego, właśnie wykonanego requestu,- a po doinstalowaniu
symfony/ai-monolog-mate-extension— przeszukiwanie logów bezgrep-owania povar/log.
To stan wydanej wersji; w dokumentacji gałęzi main czekają już kolejne, m.in. symfony-profiler-search. Projekt rozwija się szybko — warto sprawdzić mate discover u siebie zamiast wierzyć temu wpisowi na słowo.
Jedna praktyczna uwaga, która kosztowała nas przebudowę obrazu: toole profilera wymagają PHP 8.4 — proxy dla finalnych klas kolektorów stoi na natywnych lazy objects, więc na 8.2/8.3 dostaniesz Cannot generate lazy proxy for service….
Żeby docenić skalę zmiany, przypomnijmy sobie, jak debugowanie wygląda w praktyce od dwudziestu lat: var_dump($order); die;, F5, czytanie wyniku, poprawka, jeszcze raz. Dziurka od klucza — jedna zmienna, w jednym punkcie kodu, w jednym requeście, i za każdym razem trzeba modyfikować kod, a potem po sobie posprzątać. Agent przez MCP dostaje w jednym wywołaniu to, czego przez tę dziurkę nie zobaczysz nigdy: cały kontener i kompletny zapis requestu — zapytania, listenery, wyjątek ze stack trace — bez dotykania kodu aplikacji.
Pętla wygląda więc tak: agent zmienia kod → wykonuje request → pyta profiler „co się stało" → poprawia na podstawie faktów. Zamiast opisywać to teoretycznie, pokażę zapis prawdziwej sesji z projektu demo (25 klientów, 104 zamówienia; Symfony 7, Mate 0.9, narzędzia dokładnie z tego wpisu). Każdy token i każda liczba pochodzą z realnych wywołań:
> Na liście klientów w panelu admina dodaj liczbę zamówień
i datę ostatniego zamówienia każdego klienta.
● Edit templates/admin/customers.html.twig
● http-request-as GET /admin/customers → 200, profiler: eb72a6
● symfony-profiler-get eb72a6 → collector db: 26 zapytań
● Edit src/Repository/CustomerRepository.php (LEFT JOIN + COUNT + MAX, GROUP BY)
● Edit templates/admin/customers.html.twig
● http-request-as GET /admin/customers → 200, profiler: 9d2a1c
● symfony-profiler-get 9d2a1c → collector db: 1 zapytanie
✓ Lista z liczbą i datą zamówień, jedno zapytanie SQL
Zwróć uwagę, czego w tym zapisie nie ma: rundy „nie działa, popraw" i człowieka między błędem a poprawką. Pierwsza wersja była klasycznie naiwna — customer.orders|length w pętli Twig, czyli N+1: jedno zapytanie o listę plus 25 lazy-loadów. Response 200, diff wyglądał niewinnie — w klasycznym flow ten kod poszedłby do review i pewnie by przeszedł. Tu nigdy nie opuścił maszyny agenta: kolektor db pokazał 26 zapytań, agent przepisał pobieranie na jedno zapytanie z agregacją i dopiero wtedy oddał kod.
Dwie rzeczy, które Mate robi dobrze i które warto docenić projektowo: działa na izolowanym kontenerze (zadziała nawet wtedy, gdy agent właśnie zepsuł kontener aplikacji — czyli dokładnie wtedy, gdy jest najbardziej potrzebny) i automatycznie redaguje ciasteczka, nagłówki autoryzacyjne i wrażliwe zmienne środowiskowe, zanim cokolwiek trafi do modelu.
Dla porządku: to nie religia. CLI zostaje jako fallback na długi ogon jednorazowych potrzeb. Ale pętla zwrotna — to, co się wydarzyło w runtime — idzie przez MCP, bo CLI tej informacji po prostu nie ma.
Własne toole: gdzie jest granica
Mate pokrywa część generyczną. Ale każdy projekt ma tarcie, którego żadne generyczne narzędzie nie zna. Najlepszy znany mi przykład: request za zalogowanego użytkownika.
Poproś agenta o weryfikację zmiany w panelu admina i obserwuj, co robi: szuka formularza logowania, wyciąga token CSRF, składa ciasteczko sesji, czasem po drodze trafia na 2FA — i po kilku minutach błądzenia ogłasza „powinno działać", czyli poddaje się dokładnie tam, gdzie weryfikacja była najbardziej potrzebna. A przecież mamy w frameworku gotową maszynerię, której używają testy funkcjonalne: KernelBrowser. Wystarczy ją wystawić (SDK MCP dla PHP — współtworzone przez Symfony, Anthropic i PHP Foundation — sprowadza to do klasy z atrybutem):
namespace Mate;
use Mcp\Capability\Attribute\McpTool;
class AuthenticatedRequestTool
{
#[McpTool(
name: 'http-request-as',
description: 'Wykonuje request HTTP jako użytkownik o podanej roli (tylko env dev). Zwraca status, odpowiedź i token profilera.'
)]
public function execute(string $method, string $path, string $role = 'ROLE_ADMIN'): array
{
// KernelBrowser — ta sama maszyneria co w testach funkcjonalnych;
// logujemy się realnym firewallem, zwracamy status, treść
// i token profilera
}
}
Zwracany token profilera jest kluczowy: agent w następnym wywołaniu podaje go do symfony-profiler-get i ma pełny obraz tego, co jego request zrobił w środku. Dwa calle zamiast kilkunastu kroków — i, co ważniejsze, weryfikacja, która faktycznie się odbywa, zamiast być pomijana, bo była zbyt uciążliwa.
Szczegół implementacyjny z budowy demo, który może oszczędzić Ci pół godziny: loginUser() poza cyklem HTTP (serwer MCP to proces CLI) nie odtwarzał sesji między requestami. Zamiast podrabiać token taniej jest logować się prawdziwym mechanizmem — w naszym przypadku http_basic na firewallu /admin: bez danych 401, przez tool 200. Tool, który naprawdę przechodzi przez security, to też uczciwszy test niż wstrzyknięty token.
To jest też dobry test na granicę. Tool warto napisać, gdy skraca pętlę o wiele kroków (jak wyżej), gdy daje informację nieosiągalną inaczej (stan encji po requeście, payload wysłany do Messengera) albo gdy domyka domenę projektu (zasymuluj webhook płatności). Nie warto, gdy CLI załatwia sprawę jednym wywołaniem — wrapper na doctrine:fixtures:load czy cache:clear to kod do utrzymania, który nie wnosi nic ponad to, co agent i tak umie odpalić.
W tym miejscu każdy symfoniowiec myśli to samo: „mamy takie powtarzalne toole w trzech projektach — zróbmy z tego bundle". Nie róbmy. Wartościowe toole są z definicji projektowo-specyficzne; część generyczną pokrywa (i będzie pokrywał coraz szerzej) sam ekosystem symfony/ai — pisanie własnego bundla to konkurowanie z frameworkiem na jego boisku, na bardzo ruchomym terenie. A koszt napisania pojedynczego toola jest tak niski, że bundle nie ma czego amortyzować. Jeśli coś naprawdę powtarza się między projektami, właściwą formą jest konwencja i snippet w pliku instrukcji, nie zależność w composerze. A jeśli już dzielić się kodem — to PR-em do symfony/ai.
Oczy: przeglądarka po drugiej stronie pętli
Od razu rozgraniczmy, bo tu łatwo o przerost formy: do debugowania błędów przeglądarka nie jest potrzebna. Pięćsetkę agent zobaczy w zwykłym curlu, a dokładne miejsce i przyczynę — stack trace, stan requestu, zapytania — dostanie z profilera przez MCP. To pętla w całości tekstowa: szybka, tania i wystarczająca do podjęcia decyzji, co poprawić.
Przeglądarka wchodzi do gry dopiero wtedy, gdy kod już działa i pytanie brzmi nie „czy", tylko „czy dobrze wygląda". Serwer MCP sterujący Chromem przez Chrome DevTools Protocol (np. oficjalny chrome-devtools-mcp) daje agentowi oczy na warstwę, której profiler nie widzi: błędy konsoli JS, zachowanie Stimulusa czy LiveComponents, rozjechany layout na zrzucie ekranu. Backend weryfikowany profilerem, frontend — prawdziwym Chrome; obie pętle domknięte bez człowieka pośredniczącego między oknami.
Narzędzia to połowa. Druga połowa: instrukcje
Sama dostępność narzędzia nie wystarczy — agent domyślnie czyta pliki i wnioskuje z kodu, bo tak go trenowano. Trzeba mu w pliku instrukcji projektu (CLAUDE.md, AGENTS.md — zależnie od narzędzia) przestawić ten domyślny odruch.
Tu łatwo przesadzić w drugą stronę. Agent zna Symfony — prawdopodobnie „przeczytał" więcej kodu Symfony niż ktokolwiek w zespole — więc instruowanie go, że po zmianie encji wypada zwalidować schemat, a YAML warto zlintować, to szum. Takich oczywistości można napisać tysiąc linijek i każda będzie „słuszna", a wszystkie razem rozmyją to, co naprawdę ważne. Dobry test przed dopisaniem linijki: czy agent mógłby ją sam wygenerować? Jeśli tak — skasuj.
Zostaje to, czego z kodu i z wiedzy ogólnej wywnioskować się nie da: skąd w tym projekcie bierze się prawda i kiedy zadanie jest skończone.
- Źródłem prawdy o runtime jest profiler (`symfony-profiler-*`),
nie twoja hipoteza. Błąd → najpierw profiler, potem kod.
- Zmiany weryfikuj requestem przez `http-request-as`,
nie deklaracją „powinno działać".
- Zadanie jest skończone, gdy przechodzi `composer check`
i request weryfikacyjny nie pokazuje regresji w profilerze.
Kilka linijek, każda zmienia zachowanie. Pierwsza wyłącza halucynowanie przyczyn. Druga zamienia „powinno działać" w dowód. Trzecia definiuje ukończenie tak, że agent sam się rozlicza, zanim odda kod. Reszta — które serwisy są nasze, jakie konwencje obowiązują — to już normalna zawartość pliku instrukcji, niezależna od tematu tego wpisu. Zasada z zapowiedzi talka o Mate ujmuje to najkrócej: nie dawaj AI więcej kontekstu, dawaj lepszy.
Na marginesie: ta sama logika — zawężanie przestrzeni, w której agent może się mylić — działa po stronie frontendu. Biblioteka komponentów Twig (Symfony UX) zbudowana na wczesnym etapie projektu sprawia, że agent komponuje widoki z zamkniętego zestawu klocków zamiast generować za każdym razem lekko inny HTML. Ale to temat na osobny wpis.
Co z tego mamy
Efekty, które da się zaobserwować, a nie tylko zadeklarować:
- Mniej rund „nie działa, popraw" — agent weryfikuje hipotezy zamiast je halucynować.
- Wyłapywanie problemów niewidocznych w diffie — N+1, nadmiarowe listenery, deprecations. Rzeczy, które w klasycznym flow czekały na review albo na produkcję.
- Jakość przestaje zależeć od czujności recenzenta — agent ma obowiązek sam się sprawdzić, zanim odda kod, i ma czym.
I wniosek, który kieruję do sceptyków, bo sam nim byłem: nic tutaj nie wymaga wiary w AI. Profiler, kontener, lint, statyczna analiza — to są te same narzędzia, którymi sami debugujemy od lat. Zmienia się tylko operator. Cała robota polega na podpięciu istniejącej introspekcji frameworka pod protokół, którym agent umie rozmawiać, i spisaniu kilku reguł, których sami i tak przestrzegamy.
Agent bez dostępu do runtime to generator prawdopodobnego kodu. Agent z profilerem, przeglądarką i regułami weryfikacji to developer z pętlą zwrotną. Różnica między nimi to nie wersja modelu — to godzina konfiguracji po naszej stronie.
Masz pytanie albo podobny problem u siebie? Napisz: hello@incred.pl