@@ -1821,6 +1821,167 @@ public function testDefaultsCanBeCombinedWithExtraQueryParameters()
18211821 $ url ->route ('tenantPostUser ' , ['concretePost ' , 'extra ' => 'query ' ]),
18221822 );
18231823 }
1824+
1825+ public function testUrlGenerationWithOptionalParameters (): void
1826+ {
1827+ $ url = new UrlGenerator (
1828+ $ routes = new RouteCollection ,
1829+ Request::create ('https://www.foo.com/ ' )
1830+ );
1831+
1832+ $ url ->defaults ([
1833+ 'tenant ' => 'defaultTenant ' ,
1834+ 'user ' => 'defaultUser ' ,
1835+ ]);
1836+
1837+ /**
1838+ * Route with one required parameter and one optional parameter.
1839+ */
1840+ $ route = new Route (['GET ' ], 'postOptionalMethod/{post}/{method?} ' , ['as ' => 'postOptionalMethod ' , fn () => '' ]);
1841+ $ routes ->add ($ route );
1842+
1843+ $ this ->assertSame (
1844+ 'https://www.foo.com/postOptionalMethod/1 ' ,
1845+ $ url ->route ('postOptionalMethod ' , 1 ),
1846+ );
1847+
1848+ $ this ->assertSame (
1849+ 'https://www.foo.com/postOptionalMethod/1/2 ' ,
1850+ $ url ->route ('postOptionalMethod ' , [1 , 2 ]),
1851+ );
1852+
1853+ /**
1854+ * Route with two optional parameters.
1855+ */
1856+ $ route = new Route (['GET ' ], 'optionalPostOptionalMethod/{post}/{method?} ' , ['as ' => 'optionalPostOptionalMethod ' , fn () => '' ]);
1857+ $ routes ->add ($ route );
1858+
1859+ $ this ->assertSame (
1860+ 'https://www.foo.com/optionalPostOptionalMethod/1 ' ,
1861+ $ url ->route ('optionalPostOptionalMethod ' , 1 ),
1862+ );
1863+
1864+ $ this ->assertSame (
1865+ 'https://www.foo.com/optionalPostOptionalMethod/1/2 ' ,
1866+ $ url ->route ('optionalPostOptionalMethod ' , [1 , 2 ]),
1867+ );
1868+
1869+ /**
1870+ * Route with one default parameter, one required parameter, and one optional parameter.
1871+ */
1872+ $ route = new Route (['GET ' ], 'tenantPostOptionalMethod/{tenant}/{post}/{method?} ' , ['as ' => 'tenantPostOptionalMethod ' , fn () => '' ]);
1873+ $ routes ->add ($ route );
1874+
1875+ // Passing one parameter
1876+ $ this ->assertSame (
1877+ 'https://www.foo.com/tenantPostOptionalMethod/defaultTenant/concretePost ' ,
1878+ $ url ->route ('tenantPostOptionalMethod ' , ['concretePost ' ]),
1879+ );
1880+
1881+ // Passing two parameters: optional parameter is prioritized over parameter with a default value
1882+ $ this ->assertSame (
1883+ 'https://www.foo.com/tenantPostOptionalMethod/defaultTenant/concretePost/concreteMethod ' ,
1884+ $ url ->route ('tenantPostOptionalMethod ' , ['concretePost ' , 'concreteMethod ' ]),
1885+ );
1886+
1887+ // Passing all three parameters
1888+ $ this ->assertSame (
1889+ 'https://www.foo.com/tenantPostOptionalMethod/concreteTenant/concretePost/concreteMethod ' ,
1890+ $ url ->route ('tenantPostOptionalMethod ' , ['concreteTenant ' , 'concretePost ' , 'concreteMethod ' ]),
1891+ );
1892+
1893+ /**
1894+ * Route with two default parameters, one required parameter, and one optional parameter.
1895+ */
1896+ $ route = new Route (['GET ' ], 'tenantUserPostOptionalMethod/{tenant}/{user}/{post}/{method?} ' , ['as ' => 'tenantUserPostOptionalMethod ' , fn () => '' ]);
1897+ $ routes ->add ($ route );
1898+
1899+ // Passing one parameter
1900+ $ this ->assertSame (
1901+ 'https://www.foo.com/tenantUserPostOptionalMethod/defaultTenant/defaultUser/concretePost ' ,
1902+ $ url ->route ('tenantUserPostOptionalMethod ' , ['concretePost ' ]),
1903+ );
1904+
1905+ // Passing two parameters: optional parameter is prioritized over parameters with default values
1906+ $ this ->assertSame (
1907+ 'https://www.foo.com/tenantUserPostOptionalMethod/defaultTenant/defaultUser/concretePost/concreteMethod ' ,
1908+ $ url ->route ('tenantUserPostOptionalMethod ' , ['concretePost ' , 'concreteMethod ' ]),
1909+ );
1910+
1911+ // Passing three parameters: only the leftmost parameter with a default value uses its default value
1912+ $ this ->assertSame (
1913+ 'https://www.foo.com/tenantUserPostOptionalMethod/defaultTenant/concreteUser/concretePost/concreteMethod ' ,
1914+ $ url ->route ('tenantUserPostOptionalMethod ' , ['concreteUser ' , 'concretePost ' , 'concreteMethod ' ]),
1915+ );
1916+
1917+ // Same as the assertion above, but using some named parameters
1918+ $ this ->assertSame (
1919+ 'https://www.foo.com/tenantUserPostOptionalMethod/defaultTenant/concreteUser/concretePost/concreteMethod ' ,
1920+ $ url ->route ('tenantUserPostOptionalMethod ' , ['user ' => 'concreteUser ' , 'concretePost ' , 'concreteMethod ' ]),
1921+ );
1922+
1923+ // Also using a named parameter, but this time for the post parameter
1924+ $ this ->assertSame (
1925+ 'https://www.foo.com/tenantUserPostOptionalMethod/defaultTenant/concreteUser/concretePost/concreteMethod ' ,
1926+ $ url ->route ('tenantUserPostOptionalMethod ' , ['concreteUser ' , 'post ' => 'concretePost ' , 'concreteMethod ' ]),
1927+ );
1928+
1929+ // Also using a named parameter, but this time for the optional method parameter
1930+ $ this ->assertSame (
1931+ 'https://www.foo.com/tenantUserPostOptionalMethod/defaultTenant/concreteUser/concretePost/concreteMethod ' ,
1932+ $ url ->route ('tenantUserPostOptionalMethod ' , ['concreteUser ' , 'concretePost ' , 'method ' => 'concreteMethod ' ]),
1933+ );
1934+
1935+ // Passing all four parameters
1936+ $ this ->assertSame (
1937+ 'https://www.foo.com/tenantUserPostOptionalMethod/concreteTenant/concreteUser/concretePost/concreteMethod ' ,
1938+ $ url ->route ('tenantUserPostOptionalMethod ' , ['concreteTenant ' , 'concreteUser ' , 'concretePost ' , 'concreteMethod ' ]),
1939+ );
1940+
1941+ /**
1942+ * Route with a default parameter, a required parameter, another default parameter, and finally an optional parameter.
1943+ *
1944+ * This tests interleaved default parameters when combined with optional parameters.
1945+ */
1946+ $ route = new Route (['GET ' ], 'tenantPostUserOptionalMethod/{tenant}/{post}/{user}/{method?} ' , ['as ' => 'tenantPostUserOptionalMethod ' , fn () => '' ]);
1947+ $ routes ->add ($ route );
1948+
1949+ // Passing one parameter
1950+ $ this ->assertSame (
1951+ 'https://www.foo.com/tenantPostUserOptionalMethod/defaultTenant/concretePost/defaultUser ' ,
1952+ $ url ->route ('tenantPostUserOptionalMethod ' , ['concretePost ' ]),
1953+ );
1954+
1955+ // Passing two parameters: optional parameter is prioritized over parameters with default values
1956+ $ this ->assertSame (
1957+ 'https://www.foo.com/tenantPostUserOptionalMethod/defaultTenant/concretePost/defaultUser/concreteMethod ' ,
1958+ $ url ->route ('tenantPostUserOptionalMethod ' , ['concretePost ' , 'concreteMethod ' ]),
1959+ );
1960+
1961+ // Same as the assertion above, but using some named parameters
1962+ $ this ->assertSame (
1963+ 'https://www.foo.com/tenantPostUserOptionalMethod/defaultTenant/concretePost/defaultUser/concreteMethod ' ,
1964+ $ url ->route ('tenantPostUserOptionalMethod ' , ['post ' => 'concretePost ' , 'concreteMethod ' ]),
1965+ );
1966+
1967+ // Also using a named parameter, but this time for the optional parameter
1968+ $ this ->assertSame (
1969+ 'https://www.foo.com/tenantPostUserOptionalMethod/defaultTenant/concretePost/defaultUser/concreteMethod ' ,
1970+ $ url ->route ('tenantPostUserOptionalMethod ' , ['concretePost ' , 'method ' => 'concreteMethod ' ]),
1971+ );
1972+
1973+ // Passing three parameters: only the leftmost parameter with a default value uses its default value
1974+ $ this ->assertSame (
1975+ 'https://www.foo.com/tenantPostUserOptionalMethod/defaultTenant/concretePost/concreteUser/concreteMethod ' ,
1976+ $ url ->route ('tenantPostUserOptionalMethod ' , ['concretePost ' , 'concreteUser ' , 'concreteMethod ' ]),
1977+ );
1978+
1979+ // Passing all four parameters
1980+ $ this ->assertSame (
1981+ 'https://www.foo.com/tenantPostUserOptionalMethod/concreteTenant/concretePost/concreteUser/concreteMethod ' ,
1982+ $ url ->route ('tenantPostUserOptionalMethod ' , ['concreteTenant ' , 'concretePost ' , 'concreteUser ' , 'concreteMethod ' ]),
1983+ );
1984+ }
18241985}
18251986
18261987class RoutableInterfaceStub implements UrlRoutable
0 commit comments