Skip to content

[LiveComponent] urlFactory with dynamic routing not working >= 2.28.0 #2987

@haivala

Description

@haivala

after upgrading to ux-live-component > 2.27.0 I'm getting this error. "Please provide a path parameters". It happens when I'm doing $this->redirect('/'); on a live component that extends AbstractController that is on SonataPageBundle front page. It works on 2.27.0

I asked GTP-5 and it gave me this. It explains what is happening and It said that "If you need dynamic slug root and want URL mapping future-proof you should use this"

<?php

declare(strict_types=1);

namespace App\LiveComponent;

use Symfony\UX\LiveComponent\Util\UrlFactory;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
use Symfony\Component\Routing\Exception\MethodNotAllowedException;
use Symfony\Component\Routing\Exception\MissingMandatoryParametersException;

/**
 * Decorator that preserves original matched route parameters (e.g. Sonata PageBundle's
 * required "path" parameter for the dynamic CMS route) when LiveComponents rewrites URLs.
 *
 * The stock UrlFactory matches the previous URL path, extracts only the route name and
 * regenerates with JUST the path-mapped LiveProps. If the underlying route has *required*
 * parameters that were not mapped (like Sonata's page_slug "path"), generation fails.
 *
 * This subclass re-matches the previous path, captures the original parameters, merges
 * them back in (letting path-mapped LiveProps override), then proceeds with query merging.
 */
class PreservingUrlFactory extends UrlFactory
{
    public function __construct(private readonly RouterInterface $router)
    {
        parent::__construct($router);
    }

    /**
     * @param string $previousUrl      The full previous URL (path + optional query)
     * @param array<string,mixed> $pathMappedProps  LiveProps mapped to the path (mapPath = true)
     * @param array<string,mixed> $queryMappedProps LiveProps mapped to the query (default)
     */
    public function createFromPreviousAndProps(
        string $previousUrl,
        array $pathMappedProps,
        array $queryMappedProps,
    ): ?string {
        $parsed = parse_url($previousUrl);
        if ($parsed === false) {
            return null;
        }

        $previousPath = $parsed["path"] ?? "";
        $previousQueryRaw = $parsed["query"] ?? "";

        // Re-match to extract original parameters (needed for routes like Sonata's page_slug).
        [$routeName, $originalParams] = $this->matchRouteAndExtractParams(
            $previousPath,
        );
        if ($routeName === "") {
            return null;
        }

        // Merge original params with new path-mapped props (LiveProps override originals).
        $routeParams = array_merge($originalParams, $pathMappedProps);

        try {
            $newPath = $this->router->generate($routeName, $routeParams);
        } catch (ResourceNotFoundException | MethodNotAllowedException | MissingMandatoryParametersException) {
            return null;
        } catch (\Throwable) {
            return null;
        }

        // Previous query parameters
        $previousQueryParams = $this->parseQueryString($previousQueryRaw);
        // Remnant parameters the router injected into the query string
        $remnantQueryParams = $this->extractQueryParamsFromUrl($newPath);

        // Final query params (later sources override earlier)
        $finalQueryParams = array_merge(
            $previousQueryParams,
            $remnantQueryParams,
            $queryMappedProps,
        );

        return $this->replaceQueryString($newPath, $finalQueryParams);
    }

    /**
     * Match the path and return [routeName, paramsWithoutInternalKeys].
     *
     * @return array{0:string,1:array<string,mixed>}
     */
    private function matchRouteAndExtractParams(string $path): array
    {
        $context = $this->router->getContext();
        $tmpContext = clone $context;
        $tmpContext->setMethod("GET");
        $this->router->setContext($tmpContext);

        try {
            $match = $this->router->match($path);
        } catch (\Throwable) {
            $this->router->setContext($context);
            return ["", []];
        } finally {
            $this->router->setContext($context);
        }

        $routeName = $match["_route"] ?? "";
        unset($match["_route"], $match["_controller"]);

        return [$routeName, $match];
    }

    /**
     * Extract query params from a URL (ignores path & fragment).
     *
     * @return array<string,mixed>
     */
    private function extractQueryParamsFromUrl(string $url): array
    {
        $query = parse_url($url, PHP_URL_QUERY) ?? "";
        return $this->parseQueryString($query);
    }

    /**
     * @return array<string,mixed>
     */
    private function parseQueryString(string $query): array
    {
        if ($query === "") {
            return [];
        }
        $params = [];
        parse_str($query, $params);
        return $params;
    }

    /**
     * Replaces (or appends) the query string on a URL path.
     *
     * @param array<string,mixed> $params
     */
    private function replaceQueryString(string $url, array $params): string
    {
        $base = preg_replace("/[?#].*/", "", $url) ?? $url;
        if (empty($params)) {
            return $base;
        }
        $qs = http_build_query($params);
        return $base . ("" !== $qs ? "?" . $qs : "");
    }
}

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions