MidТеория6 min

Генерация URL

path(), url(), UrlGeneratorInterface, Router debugging, генерация в сервисах и CLI

Генерация URL из имени маршрута — критически важная тема. Никогда не хардкодите URL в Symfony. На экзамене проверяют все способы генерации, режимы и edge cases.

Генерация URL в контроллере

<?php

declare(strict_types=1);

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;

class NavigationController extends AbstractController
{
    public function example(): Response
    {
        // Relative path (default) — ABSOLUTE_PATH
        $url = $this->generateUrl('product_show', ['id' => 42]);
        // '/products/42'

        // Absolute URL — with scheme and host
        $url = $this->generateUrl('product_show', ['id' => 42],
            UrlGeneratorInterface::ABSOLUTE_URL
        );
        // 'https://example.com/products/42'

        // Network path — protocol-relative
        $url = $this->generateUrl('product_show', ['id' => 42],
            UrlGeneratorInterface::NETWORK_PATH
        );
        // '//example.com/products/42'

        // Relative path — from current URL
        $url = $this->generateUrl('product_show', ['id' => 42],
            UrlGeneratorInterface::RELATIVE_PATH
        );
        // '../products/42' (depends on current URL)

        return $this->redirect($url);
    }
}

Режимы генерации URL

Константа Значение Результат Пример
ABSOLUTE_PATH 1 Абсолютный путь (default) /products/42
ABSOLUTE_URL 0 Полный URL https://example.com/products/42
NETWORK_PATH 2 Без протокола //example.com/products/42
RELATIVE_PATH 3 Относительный путь ../products/42

Подвох экзамена: ABSOLUTE_PATH (1) — это DEFAULT. Не ABSOLUTE_URL (0). Многие путают: generateUrl('route') возвращает /path, а НЕ https://.... Для полного URL с доменом нужно явно передать ABSOLUTE_URL.

Extra Parameters → Query String

<?php

declare(strict_types=1);

// Route: /products (no parameters in pattern)
$url = $this->generateUrl('product_list', [
    'page' => 2,
    'sort' => 'price',
    'direction' => 'asc',
]);
// '/products?page=2&sort=price&direction=asc'

// Route: /products/{id}
$url = $this->generateUrl('product_show', [
    'id' => 42,          // Route parameter — goes into path
    'tab' => 'reviews',  // Extra parameter — goes into query string
    'ref' => 'home',     // Extra parameter — goes into query string
]);
// '/products/42?tab=reviews&ref=home'

// Fragment (#)
$url = $this->generateUrl('product_show', [
    'id' => 42,
    '_fragment' => 'reviews',
]);
// '/products/42#reviews'

Подвох экзамена: Параметры, НЕ определённые в route pattern, автоматически становятся query string. _fragment — специальный параметр для URL fragment (#). На экзамене спрашивают, куда попадёт параметр, которого нет в маршруте — ответ: в query string.

Генерация URL в Twig

{# path() — relative path (ABSOLUTE_PATH) #}
<a href="{{ path('product_show', {id: product.id}) }}">
    {{ product.name }}
</a>
{# /products/42 #}

{# url() — absolute URL (ABSOLUTE_URL) #}
<a href="{{ url('product_show', {id: product.id}) }}">
    Share this product
</a>
{# https://example.com/products/42 #}

{# Extra parameters → query string #}
<a href="{{ path('product_list', {page: 2, sort: 'name'}) }}">
    Next page
</a>
{# /products?page=2&sort=name #}

{# Fragment #}
<a href="{{ path('product_show', {id: 42, _fragment: 'reviews'}) }}">
    See reviews
</a>
{# /products/42#reviews #}

{# Current route information #}
<p>Current route: {{ app.request.attributes.get('_route') }}</p>
{# product_show #}

<p>Route params: {{ app.request.attributes.get('_route_params')|json_encode }}</p>
{# {"id":"42"} #}

{# Active link highlighting #}
<nav>
    <a href="{{ path('home') }}"
       class="{{ app.request.attributes.get('_route') == 'home' ? 'active' : '' }}">
        Home
    </a>
    <a href="{{ path('product_list') }}"
       class="{{ app.request.attributes.get('_route') starts with 'product_' ? 'active' : '' }}">
        Products
    </a>
</nav>

Подвох экзамена: path() генерирует относительный путь (/products/42), url() — абсолютный URL с доменом (https://example.com/products/42). В emails ВСЕГДА используйте url(), потому что path() без домена не работает в email-клиенте.

Генерация URL в сервисах

<?php

declare(strict_types=1);

namespace App\Service;

use Symfony\Component\Routing\Generator\UrlGeneratorInterface;

class NotificationService
{
    public function __construct(
        private readonly UrlGeneratorInterface $urlGenerator,
    ) {
    }

    public function generateProductLink(int $productId): string
    {
        return $this->urlGenerator->generate(
            'product_show',
            ['id' => $productId],
            UrlGeneratorInterface::ABSOLUTE_URL,
        );
    }

    public function generatePasswordResetLink(string $token): string
    {
        return $this->urlGenerator->generate(
            'app_reset_password',
            ['token' => $token],
            UrlGeneratorInterface::ABSOLUTE_URL,
        );
        // 'https://example.com/reset-password/abc123token'
    }
}

RouterInterface vs UrlGeneratorInterface

<?php

declare(strict_types=1);

use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Routing\RouterInterface;

// UrlGeneratorInterface — only URL generation
// Methods: generate()

// RouterInterface extends UrlGeneratorInterface + UrlMatcherInterface
// Methods: generate(), match(), getRouteCollection()

// Best practice: inject the MINIMAL interface you need
// For generation only → UrlGeneratorInterface
// For matching + generation → RouterInterface

Подвох экзамена: RouterInterface расширяет UrlGeneratorInterface. Если сервису нужна только генерация URL — инжектите UrlGeneratorInterface (принцип минимальной зависимости). Если нужен matching — RouterInterface.

Генерация URL в CLI (Console Commands)

<?php

declare(strict_types=1);

namespace App\Command;

use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;

#[AsCommand(name: 'app:send-notifications')]
class SendNotificationsCommand extends Command
{
    public function __construct(
        private readonly UrlGeneratorInterface $urlGenerator,
    ) {
        parent::__construct();
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        // In CLI, there is NO Request → no host/scheme information
        // ABSOLUTE_URL would generate: http://localhost/products/42

        // Solution 1: Configure default URI
        // config/packages/framework.yaml:
        // framework:
        //     router:
        //         default_uri: 'https://example.com'

        // Solution 2: Set context manually
        $context = $this->urlGenerator->getContext();
        $context->setHost('example.com');
        $context->setScheme('https');
        $context->setBaseUrl('');

        $url = $this->urlGenerator->generate(
            'product_show',
            ['id' => 42],
            UrlGeneratorInterface::ABSOLUTE_URL,
        );
        // 'https://example.com/products/42'

        $output->writeln('Generated URL: ' . $url);

        return Command::SUCCESS;
    }
}

Настройка default_uri

# config/packages/framework.yaml
framework:
    router:
        default_uri: 'https://example.com'
        # Used when there's no Request (CLI, Messenger)

Подвох экзамена: В CLI (Console commands, Messenger handlers) нет HTTP-запроса, поэтому ABSOLUTE_URL не знает домен. Без настройки default_uri или ручного RequestContext, ABSOLUTE_URL сгенерирует URL с localhost. На экзамене спрашивают, как генерировать URL в CLI.

Генерация URL для Messenger Handlers

<?php

declare(strict_types=1);

namespace App\MessageHandler;

use App\Message\OrderConfirmation;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;

#[AsMessageHandler]
final class OrderConfirmationHandler
{
    public function __construct(
        private readonly UrlGeneratorInterface $urlGenerator,
        private readonly MailerInterface $mailer,
    ) {
    }

    public function __invoke(OrderConfirmation $message): void
    {
        // Messenger handlers run async — no Request context
        // Requires default_uri configuration
        $orderUrl = $this->urlGenerator->generate(
            'order_show',
            ['id' => $message->orderId],
            UrlGeneratorInterface::ABSOLUTE_URL,
        );

        // Send email with link...
    }
}

Дебаг маршрутов

debug:router

# List ALL routes
php bin/console debug:router

# Output example:
# ------------- -------- -------- ------ ---------------------
#  Name          Method   Scheme   Host   Path
# ------------- -------- -------- ------ ---------------------
#  product_list  GET      ANY      ANY    /products
#  product_show  GET      ANY      ANY    /products/{id}
#  product_new   GET|POST ANY      ANY    /products/new
# ------------- -------- -------- ------ ---------------------

# Show specific route
php bin/console debug:router product_show

# Output as JSON/XML/CSV
php bin/console debug:router --format=json

# Show only routes matching a name pattern
php bin/console debug:router --show-aliases

router:match

# Test URL matching
php bin/console router:match /products/42

# Output:
# [OK] Route "product_show" matches
#
# +--------------+-------------------------------------------+
# | Property     | Value                                     |
# +--------------+-------------------------------------------+
# | Route Name   | product_show                              |
# | Path         | /products/{id}                            |
# | Path Regex   | {^/products/(?P<id>\d+)$}sDu             |
# | Host         | ANY                                       |
# | Method       | GET                                       |
# | Requirements | id: \d+                                   |
# | Defaults     | _controller: App\Controller\Product...    |
# +--------------+-------------------------------------------+

# Match with specific method
php bin/console router:match /products --method=POST

# Match with host
php bin/console router:match /products --host=api.example.com

# Match with scheme
php bin/console router:match /products --scheme=https

Типичные результаты router:match

Результат Значение
[OK] Route "name" matches Маршрут найден
None of the routes match the path "/..." 404 — URL не совпал ни с одним
Route "name" almost matches but method "..." does not match 405 — URL верный, метод нет

Подвох экзамена: router:match — главный инструмент отладки маршрутизации. Если URL совпадает с маршрутом, но метод неверный — ответ будет 405 Method Not Allowed (не 404). Команда показывает, какие маршруты "почти совпали".

Обработка ошибок генерации

<?php

declare(strict_types=1);

use Symfony\Component\Routing\Exception\RouteNotFoundException;
use Symfony\Component\Routing\Exception\MissingMandatoryParametersException;
use Symfony\Component\Routing\Exception\InvalidParameterException;

try {
    // Route doesn't exist
    $url = $this->generateUrl('nonexistent_route');
} catch (RouteNotFoundException $e) {
    // Route "nonexistent_route" does not exist
}

try {
    // Missing required parameter
    $url = $this->generateUrl('product_show'); // {id} required!
} catch (MissingMandatoryParametersException $e) {
    // Missing mandatory parameters: "id"
}

try {
    // Parameter doesn't match requirement
    $url = $this->generateUrl('product_show', ['id' => 'abc']); // \d+ required
} catch (InvalidParameterException $e) {
    // Parameter "id" must match "\d+"
}

Генерация URL с Signed URLs

<?php

declare(strict_types=1);

use Symfony\Component\HttpKernel\UriSigner;

class SecureUrlService
{
    public function __construct(
        private readonly UrlGeneratorInterface $urlGenerator,
        private readonly UriSigner $uriSigner,
    ) {
    }

    public function generateSignedUrl(int $userId): string
    {
        $url = $this->urlGenerator->generate(
            'app_verify_email',
            ['id' => $userId],
            UrlGeneratorInterface::ABSOLUTE_URL,
        );

        // Sign the URL — adds _hash parameter
        return $this->uriSigner->sign($url);
        // https://example.com/verify/42?_hash=abc123...
    }

    public function verifySignedUrl(string $url): bool
    {
        return $this->uriSigner->check($url);
    }
}

Symfony 8.0: UriSigner проверяет целостность URL через HMAC-подпись. Подпись привязана к APP_SECRET. Используется для email verification links, password reset и других одноразовых URL.


Проверь себя

Чем `path()` отличается от `url()` в Twig?

Куда попадают параметры, которых нет в route pattern, при генерации URL?

Что произойдёт при `generateUrl('product_show')` без обязательного параметра `{id}`?

Как генерировать абсолютный URL в Console Command?

Что возвращает `$this->generateUrl('product_show', ['id' => 42])` по умолчанию?