In my recent work on the Drupal Event Platform, one of the most ambitious changes has been changing the architecture to support multiple events. That means that an annual Drupal camp can retain the content of previous years while collecting session submissions for an upcoming event. It also means that the platform can support multiple events per year if needed, similar to events.drupal.org.
A lot of the work involved creating new entity references, and then making them as seamless to manage as possible, even invisible to the end user. For the variety of views that had been built to display sessions, sponsors, and so on, this meant adding contextual filters (arguments) so that the content listed would be specific to a particular event.
What I started to struggle with was how to provide navigation that would share the same simplicity. I considered approaches like creating a menu per event, but this would require an appreciable amount of manual work every time an event was created. What I wanted was a way to create a single link to a view, but have it automatically link to a version that uses a contextual filter value appropriate to the user’s current place in the site. For example, if I am in a section of a camp website talking about its 2023 iteration, I would expect links to “sessions” or “sponsors” to show listings relevant to the 2023 version. More literally, if I'm looking at a session whose path alias is something like /events/2023/sessions/stupendous-date-tricks
then I might expect the sponsors link to point to /events/2023/sponsors
.
I would need links that look for an argument within the current path. Ideally they could mimic the same sophistication and validation criteria built into views itself: in which segment of the path to look for a value, whether the value should be an id or the name of an entity, the kind of entity expected, and even a specific bundle (such as content type or vocabulary), and so on.
Towards a smarter solution
For my first iteration, I created a class that extended Drupal core’s MenuLinkDefault class. The logic to look for a relevant argument went into the getUrlObject method, and because this was written specifically for the Event Platform, it could include a number of assumptions. This worked well, as long as I set the cache context to vary by URL by adding url.alias
.
Of course, I would need more than one link. What I quickly realized was that I could provide a base class to provide all the logic, for example a version of the getUrlObject that would work of any of the links I needed:
public function getUrlObject($title_attribute = TRUE) {
// Get the current path and attempt to extract an event id.
$path = $this->requestStack->getPathInfo();
$path_token = '';
$current_event_val = NULL;
if (strlen($path) > 1) {
$path_segments = explode('/', $path);
if ($path_segments[1] === $this->prefix) {
$term_path = '/' . $this->prefix . '/' . $path_segments[2];
$path = $this->pathAliasManager->getPathByAlias($term_path);
if (preg_match('/term\/(\d+)/', $path, $matches)) {
$current_event_val = $matches[1];
// @todo validate that term found is of the expected vocabulary.
$path_token = $path_segments[2];
}
}
}
// Fall back to the site configured default event.
if (!$path_token) {
$configPage = ConfigPages::config($this->detailsPage);
if (!$configPage) {
return Url::fromUri('internal:/');
}
$current_event_val = $configPage->get($this->extractField)?->getValue();
// Extract the value from within a nested array.
while (is_array($current_event_val)) {
$current_event_val = array_pop($current_event_val);
}
$this->configPage = $configPage;
$current_event = Term::load($current_event_val);
$event_title = $current_event->label();
$path_token = $this->aliasCleaner->cleanString($event_title);
}
$url = Url::fromUri('internal:/' . $this->prefix . '/' . $path_token . '/' . $this->suffix);
return $url;
}
Then, each link only needed to extend the base class, and provide a small amount of information about where to point and what text to use for the link. For example:
<?php
namespace Drupal\event_platform_sessions\Plugin\Menu;
use Drupal\event_platform_details\Plugin\Menu\EventMenuLinkBase;
/**
* Generates a dynamic link for the schedule view.
*/
class EventScheduleMenuLink extends EventMenuLinkBase {
/**
* {@inheritdoc}
*/
protected $linkText = 'Schedule';
/**
* {@inheritdoc}
*/
protected $suffix = 'schedule';
}
Of course, the last piece necessary to get this working was an entry in a *.links.menu.yml file, such as:
event_platform_sessions.schedule_link:
weight: 0
menu_name: main
class: Drupal\event_platform_sessions\Plugin\Menu\EventScheduleMenuLink
The need for a more generic solution
This solved my immediate need for the Event Platform, but one of the other goals for this project is to move it into a set of recipes. The heart of the Event Platform has always been composable sets of configuration, which aligns beautifully with how Recipes are designed to function in the Drupal ecosystem. In addition, last year I put some substantial work into implementing config actions to place blocks into any front end or admin theme, knowing that this would make Event Platform much more flexible in its setup and use, especially with sites that use a custom theme.
When the time comes to move to recipes, I expect that the biggest part of the work will be moving the functional code that currently implements functional tools like the drag-and-drop session scheduler and the time slot generator. Wherever possible I’d like to move these into less opinionated implementations and that would then be configured by an Event Platform recipe into its current function. It’s even possible that this approach will yield more flexible versions of these tools.
For my dynamic links I wanted to move away from a class hard-coded with assumptions that align with the Event Platform. I also wanted a site owner to have the ability to create new links (or adjust the provided ones) without having to write code.
I had the idea that these dynamic links should be based on configuration entities, with code to map the provided values to links in the Drupal menu system. After a little investigation, I determined that what I needed was deriver class. For anyone unfamiliar with that term, a deriver class provides a set of data equivalent to one or more statically defined plugins. In our case, a menu link deriver replaces the need for multiple *.links.menu.yml files as described above.
I started my own exploration by reading tutorials by Daniel Sipos of WebOmelette and Phil Norton of #! Code. I found the latter particularly helpful because it documents available link attributes, examples of menu link derivers implemented in Drupal core, and more.
Getting closer
I was working on all this while I was in Leuven for Drupal Dev Days 2025. I had things mostly working, but the caching wasn’t working as well as it had with individual, hard-coded files.
At the Tuesday night Bar Crawl I found myself next to Wim Leers, so I asked who he would recommend as the best person to help with questions about dynamically generated menus. He suggested I talk to Peter Wolanin, and as fate would have it I managed to connect with him just as he was leaving the Dev Days venue before returning to the U.S.
He suggested keeping the deriver class as simple as possible, and having the complex logic around where the links should point in the link class instead. He also suggested I look at Menu Link Config as an example of a module that generates menu links from configuration entities.
Both of those turned out to be very helpful. In particular, having more of the logic within the class file made it much more straightforward to properly associate the appropriate caching metadata, and made the deriver more focused on just providing the data previously in the *.links.menu.yml files.
One more thing
Another idea I’ve been working on is having the Event Platform intelligently manage the different information that should be prioritized throughout the lifecycle of a Drupal camp or similar event. I recently collected feedback from a number of camp organizers, so we’re in a good position to start implementing the proposed workflow. This includes the following distinct phases:
- Save the Date
- Open for Sessions
- Sessions Closed
- Schedule Published
- Underway
- Complete
The Event Platform already collected most of the information necessary to perform the necessary transitions automatically. By adding the “Open for Sessions” and “Schedule Published” dates, we can now automatically move an event to a state on the date specified. The Event Platform already uses ECA so it was easy enough to create a model that will move an event between states, though it did also require defining a view with a display for each transition.
The bigger question was how to change the information displayed based on the workflow state. I wrote a patch for Moderation State Condition that allows it to work with moderated taxonomy terms, which is how Event Platform manages multiple events. I wanted to have a similar ability show and hide Smart Menu Links based on the moderation state of the referenced entity.
The trick is that we can’t add this logic into the menu class’ getUrlObject method. It is possible to return a “no link” url object using Url::fromUri('route:<nolink>')
but this wouldn’t hide the link entirely, it would show it as plain text instead. Theoretically we could duplicate the logic in the getTitle method, but this feels like an ugly hack and would probably still render some markup in the page.
Instead, I decided to leverage the isEnabled method supported by Drupal core’s MenuLinkBase. Now, if one or more workflow states have been specified for a Smart Menu Link, then within the isEnabled method it will check if the referenced entity (e.g. the value to be used as the contextual filter value in the destination). If there’s a match the menu link will be enabled, if not it will be disabled. And if no moderation state is specified the link will always be enabled, provided the referenced entity can be found.
Of course, this does also mean that the referenced entity also needs to be included in the cache tags for the menu link.
...And a few extras
As mentioned earlier I did also want to add some validation, to make sure that we’re working with the right kind of data from the URL. Smart Menu Links will likely be used with a variety of path patterns, so I thought it would also be worth being able to set an expected path segment value, as a lightweight way to validate a user is in an appropriate section of the site, before attempting to lookup and validate an entity value. Finally, a Smart Link can also provide a default value in case the validation fails. This can be an ID value, or a token that resolves to one.
The road ahead
From a UI perspective, the current configuration form is more daunting than it needs to be. Some of the optional elements should really be collapsed into details elements, to make Smart Menu Links easier to get started using.
Also, the current approach of using the URL alias as a cache context leads to an unnecessarily large number of cached values. Really the caches only need to vary based on the parts of the URL that the link needs to examine, so the greater of the segment from which to get a value or the segment used to validate the pattern. Anything after that can vary without it impacting how the link will be generated. So expect a custom cache context in an upcoming version, which the links will use instead of “url.path”.
Finally, some additional fine-tuning of the tags will probably be necessary to make sure that the links update as expected during the life cycle of an event. In the short term the ECA models that move events between workflow states also clear the cache to be certain the expected changes are visible, but ideally this won't be necessary as Smart Menu Links matures.
Feedback is welcome!
Hopefully all of this will give you some idea of why I created Smart Menu Links and how they can be useful, as well as a broader sense of how things are progressing with an ambitious set of changes to the Event Platform. On this last point, you can provide feedback in the #event-platform channel of Drupal slack, and I will also be presenting about the Event Platform at MidCamp 2025 next week. This will include a demo of getting it set up and how it changes across the stages described above, so it’s an ideal opportunity to see all these principles in action.
Comments