This feature is available in Contao 5.3 and later.
In Symfony you can generate the URL to the routes of controllers via the router service. However,
in Contao you also want to generate front end URLs for your objects (like a news item for example) - or just
for a regular page.
Contao 5.3 introduces a system with which you can implement “URL resolvers” for your own objects (models,
Doctrine entities) and then use the ContentUrlGenerator service to generate URLs to these database records - or to any of the
other existing models in Contao, like news or pages.
Content URL Generation
Since Contao 5.3 this can be done via the ContentUrlGenerator service, which works analogous to the Symfony
router service. It has a generate() method, just like the Symfony URL generator - but instead of a route name, it
expects an object for which the URL should be generated as its first parameter. You can optionally pass parameters
for the URL generation and also define the URL reference type (e.g. absolute URL, absolute path, etc.).
This is important for when your front end module references a page as the redirect target for example. Or
if your front end module renders a news list and you need the URL to the details page of each news item.
// src/MyService.php
useContao\CoreBundle\Routing\ContentUrlGenerator;useSymfony\Component\Routing\Generator\UrlGeneratorInterface;classMyService{publicfunction__construct(privatereadonlyContentUrlGenerator$contentUrlGenerator){}publicfunction__invoke(){$page=PageModel::findBy(…);// Generates an absolute URL for the given page
$pageUrl=$this->contentUrlGenerator->generate($page,[],UrlGeneratorInterface::ABSOLUTE_URL);$news=NewsModel::findBy(…);// Generates an absolute path for the given news item
$newsUrl=$this->contentUrlGenerator->generate($news,[],UrlGeneratorInterface::ABSOLUTE_PATH);}}
But suppose you have a front end module that fetches your objects from the database and then lists them via a template
in the front end:
For your own objects (i.e. for your own models or entities), you can register a “content URL resolver” which the
ContentUrlGenerator will then invoke whenever the generation of an URL is requested. Such a resolver needs to
implement the ContentUrlResolverInterface:
namespaceContao\CoreBundle\Routing\Content;useContao\PageModel;interfaceContentUrlResolverInterface{/**
* Returns a result for resolving the given content.
*
* - ContentUrlResult::url() if the content has a URL string that could be relative or contains an insert tag.
* - ContentUrlResult::redirect() to generate the URL for a new content instead of the current one.
* - ContentUrlResult::resolve() to generate the URL for the given PageModel with the current content.
*
* Return NULL if you cannot handle the content.
*/publicfunctionresolve(object$content):ContentUrlResult|null;/**
* Returns an array of parameters for the given content that can be used to
* generate a URL for this content. If the parameter is used in the page route, it
* will be used to generate the URL. Otherwise, it is ignored (contrary to the
* Symfony URL generator, which would add it as a query parameter).
*
* @return array<string, string|int>
*/publicfunctiongetParametersForContent(object$content,PageModel$pageModel):array;}
The resolver service also needs to be tagged with contao.content_url_resolver (done automatically through the
interface, if auto configuration is enabled).
Each content URL resolver will receive an object for which the URL is supposed to be generated. Each resolver then
needs to decide whether they are responsible for that type of object and otherwise return null.
Let’s have a look at Contao’s own news URL resolver as an example:
namespaceContao\NewsBundle\Routing;useContao\ArticleModel;useContao\CoreBundle\Framework\ContaoFramework;useContao\CoreBundle\Routing\Content\ContentUrlResolverInterface;useContao\CoreBundle\Routing\Content\ContentUrlResult;useContao\NewsArchiveModel;useContao\NewsModel;useContao\PageModel;classNewsResolverimplementsContentUrlResolverInterface{publicfunction__construct(privatereadonlyContaoFramework$framework){}publicfunctionresolve(object$content):ContentUrlResult|null{if(!$contentinstanceofNewsModel){returnnull;}switch($content->source){// Link to an external page
case'external':returnContentUrlResult::url($content->url);// Link to an internal page
case'internal':$pageAdapter=$this->framework->getAdapter(PageModel::class);returnContentUrlResult::redirect($pageAdapter->findPublishedById($content->jumpTo));// Link to an article
case'article':$articleAdapter=$this->framework->getAdapter(ArticleModel::class);returnContentUrlResult::redirect($articleAdapter->findPublishedById($content->articleId));}$pageAdapter=$this->framework->getAdapter(PageModel::class);$archiveAdapter=$this->framework->getAdapter(NewsArchiveModel::class);// Link to the default page
returnContentUrlResult::resolve($pageAdapter->findPublishedById((int)$archiveAdapter->findById($content->pid)?->jumpTo));}publicfunctiongetParametersForContent(object$content,PageModel$pageModel):array{if(!$contentinstanceofNewsModel){return[];}return['parameters'=>'/'.($content->alias?:$content->id)];}}
The news resolver first checks whether the given $content is an instance of NewsModel. Depending on the type of news
either of these 3 things will happen next:
If the news is simply a reference to an external URL, the news URL resolver returns a ContentUrlResult with the
stored URL as its StringUrl content. This will cause the result to be forwarded to the StringResolver, which will
then process the URL further (i.e. replace insert tags and make sure that the URL reference type matches the requested
type).
If the news redirects to a Contao page or article (and not to a news reader), a ContentUrlResult with the page or article as its content
will be returned. This will cause the result to be forwarded to the PageResolver or ArticleResolver respectively.
Otherwise the news resolver determines the target page of the news archive and returns a ContentUrlResult
with that page as its content.
The difference in the latter case is the usage of ContentUrlResult::resolve() rather than
ContentUrlResult::redirect(). In this case the URL will be resolved for the given target with the current content as
the target’s content.
This is important for the second part of the interface: getParametersForContent(). Here, your URL resolver can define
what parameters should be used during URL generation when generating the URL for your resolved content. For example, if your
content is expected to be shown on a regular page of Contao (like in the news item example) then you might want to
define the parameters parameter (see Legacy Parameters). Or if your content is expected to be
shown within a certain page controller, it knows the parameters that your page controller’s
route uses for the given content.
Custom Example
The follwing shows a custom example using a page controller. Let’s say you have a custom DCA tl_foobar and an
accompanying FoobarModel:
Now our resolver to generate URLs to these records could look like this:
// src/Routing/FoobarResolver.php
namespaceApp\Routing;useApp\Model\FoobarModel;useContao\CoreBundle\Routing\Content\ContentUrlResolverInterface;useContao\CoreBundle\Routing\Content\ContentUrlResult;useContao\PageModel;classFoobarResolverimplementsContentUrlResolverInterface{publicfunctionresolve(object$content):ContentUrlResult|null{if(!$contentinstanceofFoobarModel){returnnull;}/**
* This is a simplification and assumes your model has a property "jumpTo" that points to
* the target page. How the target page is determined will depend on your application.
*/returnContentUrlResult::resolve(PageModel::findByPk($content->jumpTo));}publicfunctiongetParametersForContent(object$content,PageModel$pageModel):array{if(!$contentinstanceofFoobarModel){return[];}return['foobarId'=>(int)$content->id];}}
As you can see within our getParametersForContent() method, we return a value for the foobarId parameter. This is
the required parameter for our page controller, for which we are letting the content URL resolver generate the URL in our
own resolver above.