Drupal 9: Programmatically Creating And Using URLs And Links

Drupal 9: Programmatically Creating And Using URLs And Links

20th March 2022 - 25 minutes read time

This is probably not relevant to some people, but I find that I'm always searching for this information when I need to print out a URL or find the current page path.

The difficulty is that finding or printing out a URL is very contextual and there is more than one way to get or use this information in Drupal. You might have a node object that you need to convert into a fully qualified path, or you want to print out the path of a route, each of which have different approaches. I think that's why there are so many questions asking variations of this topic on sites like Stackoverflow.

What is surprising to me is that there is very little documentation on drupal.org about this. Creating URLs and printing out links is perhaps the most common thing that needs to be done by developers creating themes, outside of changing the classes or markup of a block of HTML.

In this article I will go through some of the main things you should be aware of when using paths and URLs in Drupal and then look at examples of using each.

The main objects I will address here are \Symfony\Component\HttpFoundation\Request, \Drupal\Core\Url and \Drupal\Core\Link, all of which have their own uses. After that I will look at other ways in which links and URLs can be created.

The Request Object

When you respond to a page request in Drupal you will have access to a Request object. This is an instance of \Symfony\Component\HttpFoundation\Request and can be used to tell you all about the current request. As you might see from the name, this is actually a Symfony component and therefore has no knowledge about Drupal.

This object is an important starting point when looking for URL information as it's a good way of getting the current path, host, or other parameters being sent to the page.

Getting the Request object is a case of just asking Drupal for it. You can do this statically, like the following.

$request = \Drupal::request();

You can also use dependency injection to inject the service 'request_stack' into any service you are using and call the getCurrentRequest() method on that service. This will give you the access to the same object.

/** @var \Symfony\Component\HttpFoundation\Request $request */
$request = \Drupal::service('request_stack')->getCurrentRequest();

Once you have access to this object you can use it to inspect the current situation on the page.

To get the current site domain name of the site you can use a couple of methods. The getHost() method will just return the domain name.

$host = $request->getHost();
// www.example.com.

The getHttpHost() method calls the getHost() method and then will add on the port number in use if the site happens to be served through a port other than 80 (for http) and 443 (for https). For example, using this on a site being served through port 8080.

$host = $request->getHttpHost();
// www.example.com:8080

To get the current scheme of the site, which will be either http or https, you can use the getScheme() method.

$request->getScheme();
// https

To get the scheme and domain address together you can use getSchemeAndHttpHost(), which will combine together the above methods into a single method.

$fullUrl = $request->getSchemeAndHttpHost();
// https://www.example.com

If you want to get the path of the current page you can use the getRequestUri() method. This will obviously depend on the page you are visiting, but assuming that the page being visited is "https://www.example.com/some/internal/page" then the path returned will be /some/internal/page.

$requestUri = $request->getRequestUri();
// /some/internal/page.

If the getRequestUri() method is called from the front page of the site then it will return "/". The path returned here will also contain any language information, which is quite common on Drupal sites.

Finally, if you want to find the query parameters being sent to the page then the request object contains a "query" property that contains this information. This is an instance of the class \Drupal\Core\Http\InputBag and it is essentially an collection of the parameters sent to the request.

As an example, to get all of the query parameters as an array you can call the all() method on the query property.

$queryParameters = $request->query->all();
// ["parameter1" => "123"]

The request object can be used to find out all kinds of information about the request. Things like cookies, files, PHP server properties, or any headers that are sent with the request are all available in a similar way to the query property. For the purposes of finding out the current domain and path there isn't much else in the class.

The Url Object

Before going further it's a good idea to address the use of the internal \Drupal\Core\Url class. This is a powerful class and is used in quite a few places in Drupal so it's worth taking some time to get used to it.

This class isn't injected into any services inside Drupal but can be created through a number of methods or instantiated directly. If you want to use this class then you just need to add the namespace to the top of the file and start using it in your code.

use \Drupal\Core\Url;

To instantiate the Url object directly through the composer you need a route. Routes are defined in Drupal in *.routing.yml files and are essentially connections between a path and the class that responds to that path. There are a few more details to routes, but that is essentially it.

As an example, the route "user.logout" defines the path "/user/logout", and this points to the method \Drupal\user\Controller\UserController::logout. This means that is a user visits the "/user/logout" path then the logout() method will be called on that controller.

To create the Url object with this route we would do something like this.

$url = new Url('user.logout');

There are also a few special routes that are used internally to define special instances.

  • <front> - This is a direct path to the home page, which on most Drupal sites is just a slash. If your Drupal site is in a sub-directory then the front page will be that sub directory.
  • <none> - This is similar to the <front>, with the exception that it will always point to the root of the domain.
  • <current> - This is the current request path, similar to asking the Request object for the current path.
  • <nolink> - Used when the user must enter a link, but only wants the text of the link to be shown, without actually creating a link. This is useful for creating menu entries in the Drupal menu interface without having to stipulate a full link. 
  • <button> - This is similar to the <nolink> route but will display the link text in a more keyboard-accessible way.

Here is an example of creating a Url object for the current request path.

$url = new \Drupal\Core\Url('<current>');

Alternatively, it's possible to use a number of different static object creation methods within the Url class. For example, we could have written the above like this.

$url = Url::fromRoute('<current>');

This still creates a Url object, but it's a little more verbose what is going on in this method.

You can also use the fromRoute() method with a route name, by passing a route name to the method.

$url = Url::fromRoute('user.logout');

There are actually a number of static creation methods you can use within the Url class. Let's look at a few.

If you have the Request object in hand then you can convert that to a Url object through the createFromRequest() method.

$url = Url::createFromRequest($request);

If you have a full URL, perhaps from an external resource, then you can use the fromUri() method.

$url = Url::fromUri('https://www.example.com/');

You can also pass in a number of different strings at the start of the parameter here in order to use different routes or paths. This is useful if you have an internal path, a route or an entity.

To create a Url to a (valid) internal path use the "internal:" prefix.

$url = Url::fromUri('internal:/user/logout');

To create a link to a (valid) internal route use the "route:" prefix.

$url = Url::fromUri('route:user.logout');

Note: Take care here as the Url object will be created even if the internal path or route is invalid. You'll only realise that there is a problem when you come to use that Url object as Drupal will then throw an error.

If you want to create a link to a path that doesn't exist within your site then you can use the "base:" prefix.

$url = Url::fromUri('base:/some/random/path');

This will allow you to use the Url object without it throwing an error due to the path not existing.

If you are accepting user input then it's a really good idea to use the method fromUserInput() to generate the Url. This will allow you to create a Url object without creating any problems due to missing routes or invalid paths.

$url = Url::fromUserInput('/some/path');

Finally, there is a method called fromRouteMatch(), which requires a \Drupal\Core\Routing\RouteMatch object to be passed as a parameter. This is a special kind of object that represents a route, rather than loading the route itself.

To use this method you need to get the current request and then use that request to create a RouteMatch object.

$request = \Drupal::request();
$route_match = RouteMatch::createFromRequest($request);
$url = \Drupal\Core\Url::fromRouteMatch($route_match);

The Url object generated here will act just like a object created with a route.

Adding Parameters

It it also possible pass extra parameters to most of these functions in order to change how the Url object is created.

The constructor accepts a route parameters parameter that can be fed to the Url being created. This means that when using a route that accepts parameters the parameters can be added as an array to the constructor. Here is an example of creating a Url to a node entity.

$url = new Url('entity.node.canonical', ['node' => 1]);

It's also possible to change some aspects of the URL being generated. For example, to add some query parameters to the URL you can pass an array like this.

$url = new Url('entity.node.canonical', ['node' => 1], ['query' => ['parameter' => '123']]);

Most of the static methods within the class accept the options parameter. The fromRoute() method is the only method that also accepts the route parameters parameters in the same way as the constructor.

$url = Url::fromRoute('entity.node.canonical', ['node' => 1], ['query' => ['parameter' => '123']);

The fromUri() method can be used in a similar way.

$url = Url::fromUri('internal:/user/logout', ['query' => ['parameter' => '123']]);

Using The Url Object

Now that we have created the Url object we should look at what it's used for. The Url object itself can be used to alter various aspects of path or route and render the URL out as a string.

Three methods can be used to render the Url into a printable state.

The simplest option is to print the Url as a string.

$url = Url::fromUri('route:user.logout');
$string = $url->toString();
// /user/logout

If you are printing out a route to an entity then the Url class will translate this to the full aliased path of the entity.

$url = Url::fromRoute('entity.node.canonical', ['node' => 1]);
$string = $url->toString();
// /full/path/to/the/node

You can also render the URL as a Drupal route by using the toUriString() method, which will also include any information about route parameters set within the Url object. This is useful when storing the Url in a database table since this is a representation of the Url that can be converted back into a usable Url object.

$url = Url::fromUri('route:user.logout');
$uriString = $url->toUriString();
// route:user.logout

It's also possible to generate a render array using the toRenderArray() method. This can be added useful if the Url needs to be printed out to the user from within a block or controller.

$url = Url::fromUri('route:user.logout');
$renderArray = $url->toRenderArray();

If you have a Url object then you can run a number of methods to change it before it is rendered. This means you don't need to supply all of the arguments when creating the Url object, you can instead just create the Url and then apply different properties later.

For example, we can set the Url to be absolute so that when it is printed out it will contain the full server address as well as the path. Without this in place we would just get the path of the Url object rendered.

$url = Url::fromUri('user.logout');
$url->setAbsolute();
$string = $url->toString();
// https://www.example.com/user/logout

When creating routes that require parameters through the fromUri() method you can set those parameters using the setRouteParameter() method.

$url = Url::fromUri('route:entity.node.canonical');
$url->setRouteParameter('node', 1);

The Url object is only concerned with representing the URL. If we wanted to print an HTML link then we need to use another object called Link.

The Link Object

The internal \Drupal\Core\Link class is used to render links. This class must be used statically just like the Url object, and so must also be added to the top of any file that uses it.

use \Drupal\Core\Link;

Just like the Url object there are a couple of ways to create a Link object.

$link = new Link('Link', $url);

You can alternatively use the fromTextAndUrl() method to generate a Link object with the same parameters as the constructor.

$link = Link::fromTextAndUrl(t('Link'), $url);

It is possible to generate a link without having to first generate a Url object bu using the createFromRoute() static method.

$link = Link::createFromRoute(t('Link'), 'user.logout');

If you want to pass parameters or options to this method then the third and fourth parameters respectively can do this.

$link = Link::createFromRoute(t('Link'), 'entity.node.canonical', ['node' => 1], ['parameter' => '123']);

With the Link object in hand we can now do a couple of things with it.

Using The Link Object

The Link object's main function is to render a Link as a HTML element, using the supplied text and URL to create the link.

To print out a full HTML link using the passes parameters you can use the getGeneratedLink() method.

$url = Url::fromUri('route:user.logout');
$link = new Link('Log Out', $url);
$string = $link->toString();
$string->getGeneratedLink();
// <a href="/en/user/logout">Log Out</a>

Alternatively, you can use the toRenderable() method to generate a renderable array. This is useful if you want to print the link off as part of the output of a controller or form since you can just add the link to the output.

$url = Url::fromUri('route:user.logout');
$link = new Link('Log Out', $url);
$renderable = $link->toRenderable();
$build['link'] = $renderable;

With the renderable array in hand we also have the ability to add attributes to the link HTML. The following example will add a class to the HTML output.

$url = Url::fromUri('route:user.logout');
$link = new Link('Log Out', $url);
$renderable = $link->toRenderable();
$build['link'] = $renderable;
$build['link']['#attributes'] = ['class' => ['some-class']];

By adding classes and other This gives quite a bit of control over the HTML of the link from outside the template layer of the Drupal application.

Path Alias Manager Service

The path_alias.manager service often comes in useful when you want to convert between the internal path of an entity and the alias of that path. It is often used in similar situations as the Link and Url classes, so it makes sense to mention it when taking about creating URLs.

To use this service you must either inject it via dependency injection or grab the service using the service loader.

/** @var \Drupal\path_alias\AliasManager $aliasManager */
$aliasManager = \Drupal::service('path_alias.manager');

With this service we can now convert the alias of the path to the internal path using the getPathByAlias() method. As an example we can convert an alias of a node to the internal path of the node (which in this case is /node/1).

$path = $aliasManager->getPathByAlias('/some/alias/of/a/node');
// /node/1

The opposite process can be done to convert a path to an alias with the getAliasByPath() method.

$alias = $aliasManager->getAliasByPath('/node/1', 'en');
// /some/alias/of/a/node

I don't tend to use this service much since the Url and Link classes are both alias aware, but I include it here as it can be useful in some situations. You will also see some examples out there that state that this is how to get the alias of a node, which is technically correct, but it's easier to do this using the node itself.

Path Current Service

Whilst you can get the current path from the Url and Request objects there also exists a service called "path.current" that you can use to get the current path of the requested page.

/** @var \Drupal\Core\Path\CurrentPathStack $pathCurrentService */
$pathCurrentService = \Drupal::service('path.current');
$path = $pathCurrentService->getPath();
// /some/requested/path

Whilst this is useful you shouldn't rely on the path to detect certain pages. It's better to use the route to find the currently used route. This is a more accurate approach and will return more reliable results.

Current Route Match Service

To match the currently used route there exists a "current_route_match" service. You can use this service to grab information about the currently used route, including what parameters were passed to the route.

To get the current route name you can use the getRouteName() method of the service.

/** @var \Drupal\Core\Routing\CurrentRouteMatch $currentRouteMatchService */
$currentRouteMatchService = \Drupal::service('current_route_match');
$currentRouteMatchService->getRouteName();

If you want to use this service to convert the route into a Url object then you can use a method called fromRouteMatch() in the Url class. This method accepts a \Drupal\Core\Routing\RouteMatch object which we can get from the current_route_match service using the getCurrentRouteMatch() method.

Here is an example of using the different components here to generate a Url object for the current route.

/** @var \Drupal\Core\Routing\CurrentRouteMatch $currentRouteMatchService */
$currentRouteMatchService = \Drupal::service('current_route_match');
$route = Url::fromRouteMatch($currentRouteMatchService->getCurrentRouteMatch());

The getCurrentRouteMatch() is a wrapper around the function RouteMatch::createFromRequest() and gives us a handy way of generating a RouteMatch object.

Other Methods For Printing Links

There are a couple of other mechanisms to be aware of when generating URLs and printing links.

Link Aware Entities

If you are working with entities then the good news is that many entity types have free access to the methods toUrl() and toLink(). These methods will return the Url or Link associated with the entity currently loaded.

$node = Node::load(1);
$url = $node->toUrl();
$link = $node->toLink();

You can then go on to use the Url and Link objects to render links above.

Links In Translated Strings

When using the translation methods (either t() or $this->t()) you can use the Url object to inject the correct link into the text.

Here is an example of this in action.

$url = \Drupal\Core\Url::fromRoute('entity.node.canonical', ['node' => 1]);
$string = t('Link to an <a href="@url">entity</a>', ['@url' => $url->toString()]);

Links In Twig Templates

If you have a need to add routed link directly to a template then the Twig engine has access to the Url object so you can use this to generate a link in the same way. The default behaviour of the Url object when rendered is to run the toString() method, so this is the method used in Twig templates.

For example, you can print a simple link to any route on the site using the following syntax.

<a href="{{url('user.logout'}}"> {{ 'Log Out'|t }} </a>

If you want to print a link to an entity then you can also inject the parameters to the route using the following syntax.

<a href="{{url('entity.node.canonical', {'node': node.id() }) }}"> {{ 'Link to a node'|t }} </a>

Conclusion

By using the Link and Url objects you can create links in any way you need. Understanding how to use these two objects is key when you want to generate these items.

As I said at the start, generating URLs and links is quite contextual so much of what I have said here might change depending on the situation you are in. Feel free to add a comment below or contact us and we will update this post with more examples of how to generate links in your given context.

If you are stuck on how to generate a link then you can hire us to work with you to provide training and assistance to do what you need.

Comments

Permalink

Nice writeup, will share this with our team

Christoph (Sat, 03/26/2022 - 06:23)

Permalink

Great article, thank you!

Ahmed Ayman (Tue, 03/29/2022 - 20:23)

Add new comment

The content of this field is kept private and will not be shown publicly.