Problem/Motivation
We modify the Content Lock configuration in hook_modules_installed when we detect that the module is installed. We run a large multi-site platform where the module is optional. When it's installed we want to set up the configuration for it so we do it in that hook. One of the things we do is enable content locking for Nodes. However, when the user goes to edit a node, they get an exception:
Symfony\Component\Routing\Exception\RouteNotFoundException: Route "content_lock.break_lock.node" does not exist. in Drupal\Core\Routing\RouteProvider->getRouteByName() (line 214 of core/lib/Drupal/Core/Routing/RouteProvider.php).
Drupal\Core\Routing\UrlGenerator->getRoute('content_lock.break_lock.node') (Line: 290)
Drupal\Core\Routing\UrlGenerator->generateFromRoute('content_lock.break_lock.node', Array, Array, 1) (Line: 105)
Drupal\Core\Render\MetadataBubblingUrlGenerator->generateFromRoute('content_lock.break_lock.node', Array, Array, 1) (Line: 773)
Drupal\Core\Url->toString(1) (Line: 176)
Drupal\Core\Utility\LinkGenerator->generate(Object, Object) (Line: 104)
Drupal\Core\Render\Element\Link::preRenderLink(Array)
call_user_func_array(Array, Array) (Line: 107)
Drupal\Core\Render\Renderer->doTrustedCallback(Array, Array, 'Render #pre_render callbacks must be methods of a class that implements \Drupal\Core\Security\TrustedCallbackInterface or be an anonymous function. The callback was %s. See https://www.drupal.org/node/2966725', 'exception', 'Drupal\Core\Render\Element\RenderCallbackInterface') (Line: 876)
Drupal\Core\Render\Renderer->doCallback('#pre_render', Array, Array) (Line: 438)
Drupal\Core\Render\Renderer->doRender(Array, Object) (Line: 510)
Drupal\Core\Render\Renderer->doRender(Array, Object) (Line: 227)
Drupal\Core\Render\Renderer->render(Array) (Line: 491)
Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 98)
__TwigTemplate_09ecfd69393aaf199d79429693e8b7b2->block_footer(Array, Array) (Line: 446)
Twig\Template->yieldBlock('footer', Array, Array) (Line: 68)
__TwigTemplate_72d332fcdba8c4a0930ac483f8aa49ff->doDisplay(Array, Array) (Line: 402)
Twig\Template->yield(Array, Array) (Line: 51)
__TwigTemplate_09ecfd69393aaf199d79429693e8b7b2->doDisplay(Array, Array) (Line: 402)
Twig\Template->yield(Array, Array) (Line: 358)
Twig\Template->display(Array) (Line: 373)
Twig\Template->render(Array) (Line: 51)
Twig\TemplateWrapper->render(Array) (Line: 34)
twig_render_template('core/themes/claro/templates/node-edit-form.html.twig', Array) (Line: 380)
Drupal\Core\Theme\ThemeManager->render('node_edit_form', Array) (Line: 497)
Drupal\Core\Render\Renderer->doRender(Array, Object) (Line: 227)
Drupal\Core\Render\Renderer->render(Array, ) (Line: 242)
Drupal\Core\Render\MainContent\HtmlRenderer->Drupal\Core\Render\MainContent\{closure}() (Line: 627)
Drupal\Core\Render\Renderer->executeInRenderContext(Object, Object) (Line: 235)
Drupal\Core\Render\MainContent\HtmlRenderer->prepare(Array, Object, Object) (Line: 131)
Drupal\Core\Render\MainContent\HtmlRenderer->renderResponse(Array, Object, Object) (Line: 90)
Drupal\Core\EventSubscriber\MainContentViewSubscriber->onViewRenderArray(Object, 'kernel.view', Object) (Line: 246)
Symfony\Component\EventDispatcher\EventDispatcher::Symfony\Component\EventDispatcher\{closure}(Object, 'kernel.view', Object) (Line: 206)
Symfony\Component\EventDispatcher\EventDispatcher->callListeners(Array, 'kernel.view', Object) (Line: 56)
Symfony\Component\EventDispatcher\EventDispatcher->dispatch(Object, 'kernel.view') (Line: 188)
Symfony\Component\HttpKernel\HttpKernel->handleRaw(Object, 1) (Line: 76)
Symfony\Component\HttpKernel\HttpKernel->handle(Object, 1, 1) (Line: 53)
Drupal\Core\StackMiddleware\Session->handle(Object, 1, 1) (Line: 48)
Drupal\Core\StackMiddleware\KernelPreHandle->handle(Object, 1, 1) (Line: 28)
Drupal\Core\StackMiddleware\ContentLength->handle(Object, 1, 1) (Line: 32)
Drupal\big_pipe\StackMiddleware\ContentLength->handle(Object, 1, 1) (Line: 40)
Drupal\ps_ban\BanMiddleware->handle(Object, 1, 1) (Line: 48)
Drupal\Core\StackMiddleware\ReverseProxyMiddleware->handle(Object, 1, 1) (Line: 51)
Drupal\Core\StackMiddleware\NegotiationMiddleware->handle(Object, 1, 1) (Line: 53)
Drupal\Core\StackMiddleware\AjaxPageState->handle(Object, 1, 1) (Line: 51)
Drupal\Core\StackMiddleware\StackedHttpKernel->handle(Object, 1, 1) (Line: 715)
Drupal\Core\DrupalKernel->handle(Object) (Line: 19)
The route isn't registered. Content Lock has a config subscriber that triggers a route rebuild when its config is changed. That rebuild runs during the terminate phase of the same request where the config was saved, but the ContentLock service instance in the container still holds a stale config snapshot (the one from before the save, with types: {}). So ContentLockRoutes::routes() calls hasLockEnabled('node') which returns FALSE so no content_lock.break_lock.node route is generated. On the next request, the ContentLock service is freshly constructed with the current config, so the form alter correctly tries to use the route, but it was never generated.
Steps to reproduce
Add a hook_modules_installed hook implementation where you configure Content Lock and save it like this:
if (in_array('content_lock', $modules)) {
$contentLockSettings = \Drupal::configFactory()->getEditable('content_lock.settings');
$contentLockSettings->set('verbose', FALSE);
$enabledTypes = $contentLockSettings->get('types');
// Enable only for nodes.
$enabledTypes['node'] = ['*' => '*'];
$contentLockSettings->set('types', $enabledTypes);
// Ensure only edit and layout builder form modes use locking.
$formOpLock = $contentLockSettings->get('form_op_lock');
$formOpLock['node'] = [
'mode' => 1,
'values' => [
'default' => 'default',
'edit' => 'edit',
'layout_builder' => 'layout_builder',
],
];
$contentLockSettings->set('form_op_lock', $formOpLock);
$contentLockSettings->save();
Proposed resolution
Update the ContentLock service to load the config object fresh for each method invocation that uses it. That way it's reread as needed during the same request.
Remaining tasks
User interface changes
API changes
Data model changes
Issue fork content_lock-3589835
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
Comments
Comment #3
bkosborneComment #4
bkosborneComment #5
bkosborneTest fails seem unrelated
Comment #7
alexpottYeah I seen issues like this before. It's a shame but this fix is pragmatic.
Comment #9
alexpott