Dzień 5: Routing
W Dniu 4 zbudowaliśmy dwie akcje (list i show) i połączyliśmy je linkami przez funkcję path().
Dziś zrozumiemy, jak to działa: jak Symfony dopasowuje adres URL do akcji, jak definiować wymagania
i metody HTTP, jak generować adresy z nazw tras i jak debugować routing.
W Symfony 8 routing definiujemy atrybutami
#[Route]bezpośrednio przy akcjach. Plikconfig/routes.yamljedynie wskazuje Symfony, gdzie te atrybuty szukać (type: attribute).
Jak działa routing
Gdy klikniesz ofertę na stronie głównej, adres wygląda tak: /job/1. Skąd Symfony wie, którą akcję
wywołać i skąd bierze obiekt Job?
Cała ścieżka żądania:
- Żądanie trafia do front controllera
public/index.php. - Router dopasowuje ścieżkę URL (
/job/1) do trasy zdefiniowanej atrybutem#[Route]. - Symfony wywołuje odpowiednią akcję kontrolera, przekazując parametry z URL (
id = 1). - EntityValueResolver na podstawie
idwczytuje obiektJobi wstrzykuje go do akcji.
Adresy URL generujemy odwrotnie — z nazwy trasy (np. job_show) funkcją path(). Dzięki temu
nigdy nie wpisujemy ścieżek na sztywno.
Konfiguracja routingu
W Symfony 8 trasy definiujemy atrybutami przy akcjach, a plik config/routes.yaml mówi tylko, skąd
je wczytać:
# config/routes.yaml
controllers:
resource:
path: ../src/Controller/
namespace: App\Controller
type: attribute
Ten wpis ładuje trasy ze wszystkich kontrolerów w src/Controller/ (i podkatalogach), opisanych
atrybutami.
Zmiana vs Symfony 4.2: oryginał używał pliku
config/routes/annotations.yamlztype: annotation(adnotacje w DocBlockach). Dziś jest toconfig/routes.yamlztype: attribute— trasy to natywne atrybuty PHP 8, aSensioFrameworkExtraBundlenie jest potrzebny.
Trasy w JobController
Przypomnijmy definicje z Dnia 4 — cała konfiguracja trasy mieści się w atrybucie nad akcją:
#[Route('/', name: 'job_list', methods: ['GET'])]
public function list(JobRepository $jobs): Response
{
// ...
}
#[Route('/job/{id}', name: 'job_show', methods: ['GET'], requirements: ['id' => '\d+'])]
public function show(Job $job): Response
{
// ...
}
Zmienną w ścieżce zapisujemy w klamrach: {id}. Dla /job/1 parametr id przyjmie wartość 1.
Nazwa trasy (name) służy do generowania URL-i — to jej używamy w path().
Konwencja nazw: trasy nazywamy małymi literami z podkreśleniem, np.
job_list,job_show. Generatormake:controllerdomyślnie dodaje prefiksapp_(np.app_job_list) — obie konwencje są poprawne, ważne aby trzymać się jednej.
Prefiks na poziomie klasy
Gdy wiele tras dzieli wspólny prefiks ścieżki i nazwy, można go wynieść na poziom klasy:
#[Route('/admin', name: 'admin_')]
final class DashboardController extends AbstractController
{
#[Route('/jobs', name: 'jobs')] // ścieżka: /admin/jobs, nazwa: admin_jobs
public function jobs(): Response { /* ... */ }
}
W naszym JobController nie stosujemy prefiksu klasowego, bo lista ofert jest stroną główną (/),
a szczegóły mają ścieżkę /job/{id}. Prefiks przyda się później, np. w sekcji administracyjnej
(/admin).
Wymagania parametrów (requirements)
Router waliduje parametry wyrażeniami regularnymi. W trasie job_show wymusiliśmy, aby id było liczbą:
#[Route('/job/{id}', name: 'job_show', requirements: ['id' => '\d+'])]
Dzięki \d+ trasa dopasuje /job/1, ale nie /job/abc — w tym drugim przypadku router jej nie
dopasuje (i zwróci 404 lub sprawdzi kolejne trasy).
Zmiana vs Symfony 4.2: wymagania podajemy jako nazwany argument atrybutu (
requirements: ['id' => '\d+']) zamiast składni adnotacjirequirements={"id" = "\d+"}.
Metody HTTP
Trasę można ograniczyć do wybranych metod HTTP. Nasze strony tylko wyświetlają dane, więc akceptujemy
wyłącznie GET:
#[Route('/', name: 'job_list', methods: ['GET'])]
#[Route('/job/{id}', name: 'job_show', methods: ['GET'], requirements: ['id' => '\d+'])]
Gdy dojdziemy do formularzy (dodawanie oferty), dodamy trasy z methods: ['GET', 'POST'].
Generowanie adresów URL
Zamiast wpisywać ścieżki ręcznie, budujemy je z nazwy trasy. Dzięki temu zmiana ścieżki w atrybucie nie wymaga poprawiania linków w całej aplikacji.
W szablonie Twig:
{# ścieżka względna: /job/1 #}
<a href="{{ path('job_show', { id: job.id }) }}">{{ job.position }}</a>
{# adres absolutny: http://localhost:8090/job/1 #}
<a href="{{ url('job_show', { id: job.id }) }}">...</a>
W kontrolerze:
// przekierowanie do innej trasy
return $this->redirectToRoute('job_list');
// samo wygenerowanie URL-a
$url = $this->generateUrl('job_show', ['id' => $job->getId()]);
W dowolnym serwisie wstrzykujemy UrlGeneratorInterface:
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
public function __construct(private readonly UrlGeneratorInterface $urlGenerator) {}
$url = $this->urlGenerator->generate('job_show', ['id' => 1]);
Zmiana vs Symfony 4.2: w akcji używamy
redirectToRoute()igenerateUrl()zAbstractController, a w serwisach —UrlGeneratorInterface. W Twigupath()(względny) iurl()(absolutny) działają jak wcześniej.
Debugowanie tras
Podczas pracy z routingiem przydają się dwie komendy. Lista wszystkich tras:
docker compose exec app php bin/console debug:router
Szczegóły jednej trasy (ścieżka, metody, wymagania):
docker compose exec app php bin/console debug:router job_show
Sprawdzenie, która trasa dopasuje dany URL (bardzo pomocne przy diagnozie 404):
docker compose exec app php bin/console router:match /job/1
Wskazówka: trasy Web Debug Toolbara i Profilera (
/_wdt,/_profiler) też zobaczysz wdebug:router— są ładowane tylko w środowiskudevzconfig/routes/.
Podsumowanie
Rozumiemy już routing w Jobeet:
- ✅ trasy definiowane atrybutem
#[Route], wczytywane przezconfig/routes.yaml(type: attribute), - ✅ parametry ścieżki (
{id}) z walidacją przezrequirements, - ✅ ograniczenie metod HTTP (
methods: ['GET']), - ✅ generowanie URL-i z nazw tras:
path()/url(),generateUrl(),redirectToRoute(), - ✅ debugowanie:
debug:routerirouter:match.
W Dniu 6 wrócimy do modelu: przeniesiemy zapytania do repozytoriów, nauczymy się QueryBuilera i odfiltrujemy tylko aktywne, opublikowane oferty (zgodnie z historią F1).