Drupal by default uses user UIDs in its paths to retrieve a user object, such as "/user/UID/edit". When doing that, the route match interface is actually able to load the user object for you from the UID passed as argument in the URL. It's convenient, but sometimes for a better user experience you'd like to use the username in place of the UID in the path, without interfering with other Drupal processes. The end goal is to let users accessing user pages using "/user/USERNAME" for example.
Well, our best option here is to take the process at a very early stage of the request, implementing a path processor which deals with inbound and outbound requests. What we will do is change the USERNAME passed in the path into its UID, so we don't have to alter any other Drupal core functions.
First, let's see the entire class and a bit of explanation about inbound and outbound requests:
namespace Drupal\your_module\PathProcessor;
use Drupal\Core\PathProcessor\OutboundPathProcessorInterface;
use Drupal\Core\PathProcessor\InboundPathProcessorInterface;
use Drupal\Core\Render\BubbleableMetadata;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Path processor.
*/
final class PathProcessor implements InboundPathProcessorInterface, OutboundPathProcessorInterface {
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* Class constructor.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager) {
$this->entityTypeManager = $entity_type_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity_type.manager')
);
}
/**
* {@inheritdoc}
*/
public function processInbound($path, Request $request) {
// Inbound requests are the requests entered by the end user.
// We expect the end user to pass a username as parameter here.
// What we want to do is take this USERNAME parameter and turn it
// into its UID again, so we don't interfere with Drupal core.
}
/**
* {@inheritdoc}
*/
public function processOutbound($path, &$options = [], Request $request = NULL, BubbleableMetadata $bubbleable_metadata = NULL) {
// Outbound requests are what is returned from Drupal
// after processing the inbound ones (the one "entered" by the end user).
// We expect from Drupal to return us a UID, but we want to display the
// USERNAME to end user. So we'll do this change in this function.
}
}
Now let's see the detail of these functions:
/**
* {@inheritdoc}
*/
public function processInbound($path, Request $request) {
// Use username for {user} route parameter.
// We only want to target the /user paths and username is expected
// to be in second position (/user/USERNAME/whatever/other/slugs).
if (strpos($path, '/user') === 0) {
$path_parts = explode('/', $path);
// The end user entered a username in the second
// position of the URL, so we retrieve the user from it
// to replace it by its UID.
$username = $path_parts[3];
$users = $this->entityTypeManager->getStorage('user')->loadByProperties([
'name' => $username,
]);
if ($user = reset($users)) {
// We put back the UID, so Drupal can treat the request
// like the one it expects: "user/UID"
$path_parts[3] = $user->id();
$path = implode('/', $path_parts);
}
}
return $path;
}
/**
* {@inheritdoc}
*/
public function processOutbound($path, &$options = [], Request $request = NULL, BubbleableMetadata $bubbleable_metadata = NULL) {
// Use username for {user} route parameter.
if (isset($options['route']) && strpos($path, '/user') === 0) {
$path_parts = explode('/', $path);
// At this stage of an outbound URL we have the UID coming from Drupal.
// We want to display the username to the end user, so we retrieve it.
$uid = $path_parts[3];
/** @var \Drupal\user\Entity\User $user */
$user = $this->entityTypeManager->getStorage('user')->load($uid);
if ($user) {
// Set the username in place of the UID.
$path_parts[3] = $user->getAccountName();
$path = implode('/', $path_parts);
}
}
return $path;
}
And you can take this approach for any of "alteration" of URLs you'd want to do, for any entity and for any paths. Using path processor is the "go to" option when you want to change the form of Drupal URL system.