Account created on 8 June 2006, about 19 years ago
#

Merge Requests

More

Recent comments

🇨🇭Switzerland znerol

I managed to reduce the main branch in 📌 Introduce email yaml plugin and an email building pattern known from controllers Active to around 600 LOC (without tests). I'm going to update the issue summary over there to better reflect the current state.

🇨🇭Switzerland znerol

I'm trying to reduce the scope of the Draft PR in order to make it smaller and easier to review. As a first step, I'll be extracting features which aren't absolutely necessary to land in the first iteration. I'm thinking of the following things:

  • Support for controller closure (already removed)
  • Customizable Email class (allows us to get rid of the custom plugin factory).
  • Move the builder code to another PR. While the builder simplifies the call-sites a lot, it doesn't need to be committed right away I think.

Anyone has additional suggestions?

🇨🇭Switzerland znerol

I pushed a little refactoring of the renderer class. There is now an EmailControllerParams object passed through the whole process. This gets rid of the repeated ad-hoc $context definitions inside the render() function.

It also separates the langcode into an (alterable) environment parameters array. A hook_email_params implementation now can actually modify the language of the mail. The language switching itself is not yet implemented though.

🇨🇭Switzerland znerol

In the MR. There is a requirements check for unknown / misspelled password algorithms.

When an algorithm is configured which isn't available on the system, attempting to create new password hash will result in a fatal:

Fatal error: Uncaught ValueError: password_hash(): Argument #2 ($algo) must be a valid password hashing algorithm in Command line code on line 1

Verifying existing hashes still works (assuming the hashing algorithm is available on the system).

🇨🇭Switzerland znerol

Re #8 / #9, the 3539651-core-emails branch now contains a SystemEmailHooks implementation (an example on how this could work):

https://git.drupalcode.org/issue/drupal-3539651/-/compare/11.x...3539651...

🇨🇭Switzerland znerol

By the time this ships, every single site which upgrades will generate a requirements warning. The requirements warning points to a CR which contains example code for a service provider class. The example code for the service provider class is there because I think we shouldn't instruct people to manually add a container parameter for this. The reason I think so is that it is safer to use the \PASSWORD_* constants instead of let people attempt to hard code the exact password algorithm into a yml file by hand.

Regarding the UI: This could be just a password_argon2 module with its own requirements check and a service provider which adds the password.algoritm: argon2id container parameter.

🇨🇭Switzerland znerol

This code suggests that mail ID, parameters, and langcode will not be available at the point the email is being sent.

Good point. I think those should be added as tags / metadata to the Symfony Email object. This information is then also available on external mail service providers for filtering and routing.

🇨🇭Switzerland znerol

Thanks for the review.

Since this has come up multiple times now, I guess I'm going to iterate on the call site now. There are a couple of well known design patterns which might help in this situation. In my eyes, the current MailManagerInterface is a facade. It provides a convenience method (mail()) which abstracts away the whole complexity of mail plugins, formatting and logging.

Looking through core, I can find another well known design pattern which simplify access to complex subsystems. E.g., EntityStorageInterface::getQuery() returns an instance of a class implementing the builder pattern. The call site of entity queries resembles the example snippets by @zengenuity and @adamps. I think the builder pattern could be useful. E.g., a call site in the contact mail handler could look like this:

    $key_prefix = $message->isPersonal() ? 'user' : 'page';
    try {
      $email = $this->emailManager->getEmailBuilder()
        ->id('contact.' . $key_prefix . '_mail')
        ->param('message', $message)
        ->langcode($recipientLangcode)
        ->build() # returns a Symfony Email
      $this->mailer->send($email->to(...$recipients)->replyTo($message->getSenderMail()));
    }
    catch [...]

In cases where a message needs to be sent to multiple recipients with different languages, the builder can be reused (update module):

    $builder = $this->emailManager->getEmailBuilder()
      ->id('update.status_notify')
      ->params($items);

    foreach ($recipients as $recipient) {
      [...]
      try {
        $email = $builder->langcode($langcode)->build();
        $this->mailer->send($email->to($recipient));
        $sent++;
      }
      catch [...]

In a second step, addressing, sending and exception logging could be moved into a convenience method. It doesn't need to cover all cases (build() is still there) but it could simplify the most used ones:

Contact example:

    $key_prefix = $message->isPersonal() ? 'user' : 'page';
    $result = $this->emailManager->getEmailBuilder()
      ->id('contact.' . $key_prefix . '_mail')
      ->param('message', $message)
      ->langcode($recipientLangcode)
      ->send(to: $recipients, replyTo: $message->getSenderMail(), logger: $this->logger);

Update example:

    $builder = $this->emailManager->getEmailBuilder()
      ->id('update.status_notify')
      ->params($items);

    foreach ($recipients as $recipient) {
      [...]
      if ($builder->langcode($langcode)->send(to: $recipient)) {
        $sent++;
      }

Noted the other comments in #6. All of them are useful as well, thanks!

🇨🇭Switzerland znerol

This is a very weird issue. These are my observations so far:

Before 📌 Avoid reading session from the database multiple times during a request Needs work (git switch --detach 4e26ae9cc3da8b8a90561716c4a53fdd6c07f8f6):

  • Session cookies are refreshed when big pipe module is enabled (e.g., standard install)
  • Session cookies are not refreshed when big pipe module is not enabled (e.g., minimal install)

After 📌 Avoid reading session from the database multiple times during a request Needs work (git switch --detach 522404706e440106fe7e5d382ce018b0f89637d4):

  • Session cookies are not refreshed when big pipe module is enabled (e.g., standard install)
  • Session cookies are not refreshed when big pipe module is not enabled (e.g., minimal install)

At least this is now consistently working as expected by @cilefen.

🇨🇭Switzerland znerol

I realized, that the OP reported something different:

In 10.2.x, the session cookie is updated if necessary on every request (not yet sure whether this is done by PHP, Symfony or Drupal).

After 📌 Avoid reading session from the database multiple times during a request Needs work , the call to \Symfony\Component\HttpFoundation\Session\Session::save() is too late to update the session cookie on big pipe responses. Response headers are already sent when big pipe performPostSendTasks() is called. I can reproduce this behavior by adding a cookie_lifetime: 30 container parameter (and still using session_write_interval: 20).

We could maybe fix that by adding more logic to \Drupal\Core\StackMiddleware\Session::handle(). E.g., trigger a session save unconditionally when the session timestamp is older than session_write_interval.

🇨🇭Switzerland znerol

I tried to reproduce the issue with the following setup on 11.x:

  • Standard install profile.
  • Create a new user without admin privileges.
  • Login with the new user.
  • Repeatedly reload a page which triggers big pipe (e.g. /contact).

In order to make the manual testing reproducible, I choose the following options for PHP session gc and Symfony session metadata_update_threshold (the latter is called session_write_interval in Drupal and it is a setting):

In sites/default/services.yml:

parameters:
  session.storage.options:
    gc_divisor: 1
    gc_maxlifetime: 30
    gc_probability: 1

In sites/default/settings.php:

$settings['session_write_interval'] = 20;

The effect of the PHP gc_divisor: 1 and gc_probability: 1 is that gc is run on every request before any session data is read from the database. That means that any session record with a timestamp older than 30 seconds is removed at the start of ever request (except for requests answered by the page cache of course).

The effect of the metadata threshold is that a session write is enforced for sessions with a timestamp older than 20 seconds.

With this setup I can confirm that every request which hits the window between the metadata threshold (session_write_interval and the gc max lifetime will bump the effective session lifetime by 30 seconds (gc_maxlifetime). This is expected behavior.

I can confirm that a session is removed and a user is logged out when I leave a tab open for more than 30 seconds (gc_maxlifetime) before I reload the browser (though, see the caveat at the end). That is expected behavior as well.

In my opinion this issue can be closed. I believe that the behavior observed by the OP is a result of a gc_maxlifetime value lower/equal than session_write_interval. If there is no window between the two values, the session timestamp is only updated when new session data is written. In cases where a very short session lifetime is required, it is probably best to set session_write_interval=0. With that setting, sessions will be written on every request - no matter whether the session data has changed or not.

For the latter test, I discovered an interesting edge case. For the test to pass there needs to be other activity which triggers the gc in between the requests in the tab left open. If the session record is still in the database when the request starts, then the session record is removed from the database (which is expected), but it is written at the end again and as a result survives the timeout (which is unexpected).

I'm unsure whether or not this edge case needs a fix. With the probabilistic gc in use today, sites cannot rely on session records being deleted reliably after surpassing the gc max lifetime anyway.

🇨🇭Switzerland znerol

1) These lines seem to happen a lot. I feel it should be just one line to send a mail. The underlying layer can handle the separate build/render/send.

True. The call site needs more attention at some point. I'd like to keep the question open for a while to see what kind of abstraction best fits these cases. It very much depends also on whether or not we want to support passing a closure to $emailManager->getEmailDefinition().

2) I'm confused by inconsistencies between the modules, they seem far from following a pattern. Some have a builder some not. I feel that one class is enough and it can have separate functions for the parts. All modules can have the same 2 functions.

I tried to present many different approaches in 3539651-core-emails. E.g., the confusingly named ActionMailBuilder class is an action plugin and a mail builder at the same time (demoing the closure approach). The UpdateMailBuilder shows that it is still possible to create text-only emails (question is whether we want to support that or not). UserNotificationHandler shows the closure approach with first class callable syntax, ContactMailHandler shows how it could look like if the "controller" is set via yml file.

If we want to continue with this approach for a bit longer, we should probably decide which pattern is to become the standard one.

🇨🇭Switzerland znerol

The $langcode is tricky and indeed merits an in-depth discussion. I think it is worthwhile to consider it thoroughly whether it should remain a required parameter (like it is today, and like it was on all my prototypes) or whether it should be dropped (as suggested by some comments). And if we decide to drop $langcode from the call-site method signature, we should discuss that further with language subsystem maintainers I guess.

From the top of my head, I can think of at least four ways to determine the language for an outgoing mail:

  1. Use the site default language.
  2. Use the negotiated language (this only works if the mail sending is triggered in response to a browser request, it falls back to 1 on cron runs).
  3. Use the recipients preferred language (if the message has only one recipient and it is linked to a known user account).
  4. Use a language stored on a related entity (e.g., the language could be stored on a newsletter subscription).

It seems to me that the choice of language is context dependent. This could be an indicator that indeed the call-site is the most appropriate location to set the langcode. At the call-site, the negotiated language is still available (via ConfigurableLanguageManager::getCurrentLanguage()). Depending on how the context switching is implemented, the negotiated language might return a different result inside the mail builder (i.e., inside the switched context). That in turn means, that mail builders have less options to choose from when attempting to determine the correct message language.

The call-site also might be aware of whether or not it is running in cron (update) or from within a form submission. Issues like 🐛 Password reset invalid mail notify language Needs work also indicate, that some users expectations are different than what others find intuitive.

Keeping the $langcode on the call-site method signature forces developers to make a sensible choice. It is for sure debatable whether or not this is a good thing.

On the other hand, neither the old interface nor any of my prototypes has references to other i18n information. E.g., for certain messages it would be important to know the recipients timezone (e.g., calendar invites). So why do we have $langcode as a required parameter on the call-site method signature but not $timezone?

🇨🇭Switzerland znerol

Added the requirement checks.

Should we add a config and a proper UI to make these requirement checks more actionable? Maybe an additional Password fieldset in admin/config/people/accounts?

🇨🇭Switzerland znerol

Any call to the old function continues to work as-is but doesn't go through the new API. It would mean they wouldn't use the new mail API once replaced, but that's perfectly fine IMHO, especially as long as the whole thing is still experimental.

Exactly. This is the responsibility of NotificationHandler::sendCompat(). A replacement service implemented by the mailer module either could override it (and throw an exception to make it explicit that _user_mail_notify() isn't supported by the experimental mailer). Or it could leave it alone and then a mail would be created and sent by the legacy mail system.

🇨🇭Switzerland znerol

The individual functions are fine. They provide a good place for docs and we get rid of the $op strings at the call site.

I updated the issue summary and the change record. This is ready from my side.

🇨🇭Switzerland znerol

I went through a gitlab code search ( #3051266: Searching for code across all of Gitlab ) in order find out how _user_mail_notify is used in contrib. In order to exclude core clones and distros, I used the following query:

_user_mail_notify -path:core/modules/user -path:modules/contrib

That returns 228 results. I went through half of them and manually assessed how the function is being used. There are two bigger groups and a couple of interesting special cases.

Modules dealing with user registration

Modules dealing with password resets

General purpose / all ops

Special cases

GSIS (GOV.GR) Login

They are actually in the first group (Modules dealing with user registration) but they do just call $account->activate(); to trigger the registration email.

Own implementation

The following modules ship their own version of _user_pass_notify.

Custom op

The Forgot username module actually did implement a custom op (sight!).

I think we cannot deprecate _user_mail_notify as a whole without providing proper APIs to cover most of these use cases first. That said, I think the best way forward would be to deprecate custom ops here in a first step.

🇨🇭Switzerland znerol

In the new mail system, Contrib would like to be able to trigger generation of emails. I believe Easy Email might include a UI for this feature generally. Commerce has it specifically as "resend receipt".

Yes, of course.

🇨🇭Switzerland znerol
% ./vendor/bin/drush updb
 -------- ------------------ ------------- ------------------------------------
  Module   Update ID          Type          Description
 -------- ------------------ ------------- ------------------------------------
  system   password_kernel_   post-update   Rebuild the container after adding
           parameters                       new kernel parameters.
 -------- ------------------ ------------- ------------------------------------


 ┌ Do you wish to run the specified pending updates? ───────────┐
 │ Yes                                                          │
 └──────────────────────────────────────────────────────────────┘

>  [notice] Update started: system_post_update_password_kernel_parameters
>  [notice] Update completed: system_post_update_password_kernel_parameters
 [success] Finished performing updates.
🇨🇭Switzerland znerol

CR is ready. Also pushed an update to the StackedKernelPass docs.

🇨🇭Switzerland znerol

This is literally a beer idea, curtsy of @jurgenhaas . I'm recording this here just for completeness:

If we think 📌 Introduce email yaml plugin and an email building pattern known from controllers Active one step further, email could just be one of the supported response formats. In order to render an email, create a Request object with appropriate attributes, dispatch it through the http kernel as a subrequest, grab the email object from the response and send it via the transport framework to wherever its needed.

On the way through the request / response cycle, parameter upcasting, language negotiation, theme negotiation, render context isolation, render caching and maybe even cooperative multitasking via fibers is performed just like with every other request. Without experimentation its impossible to know what works and what breaks, though.

It would also be interesting to explore whether or not this code flow would simplify access checking on embedded attachments and images.

Considering email builders as routes (or microservices, if you prefer) also raises interesting questions. For example, some email builders likely are idempotent (simple notifications are always the same when generated with the same input) while others are not (the password reset link must be newly generated).

🇨🇭Switzerland znerol

MaintenanceModeSubscriber uses BareHtmlPageRenderer nowadays.

🇨🇭Switzerland znerol

I started working on a new approach for the core mail building infrastructure: 📌 Introduce email yaml plugin and an email building pattern known from controllers Active

I think this should feel quite familiar for people who are used to implement their own controllers.

In this iteration I also put more effort in the HTML/templating part. I took a look at the entity templates (node.html.twig, taxonomy-term.html.twig, media.html.twig, etc.). All of them use a contents variable containing a list of content items. I think it would be good to use the same pattern in email.html.twig.

🇨🇭Switzerland znerol

I moved the language switching to the language module. On monolingual sites this is now a no-op.

Also found a use case for this in core: HelpTopicSection::renderTopicForSearch().

🇨🇭Switzerland znerol

Updated the deprecation message and the change record. _user_mail_notify() is replaced by an internal service. It follows, that there is no replacement API, this must be reflected in the CR.

🇨🇭Switzerland znerol

I restored the BC for _user_email_notify() and also the return type behavior. Dispatching op to method call in _user_mail_notify() is actually more work than keeping BC.

I also found a way to remove the interface. It is possible to mark methods for setter injection with the #[Required]. That way, a subclass doesn't need receive and pass on a MailManager via its constructor. It only needs to declare the services which it really needs.

I tried to update the issue summary in order to be more precise about the intention here. Regarding #20, this issue is about a different abstraction layer. It has no influence on the design of a future mail building layer.

Regarding the discussion whether this should be enums or separate methods: 🐛 Use email verification when changing user email addresses Needs work would definitely profit from separate methods. The new email address could be passed in via an additional argument. The current MR has to tunnel it via the param array. This could be solved much better with a method with dedicated signature: https://git.drupalcode.org/project/drupal/-/merge_requests/5773/diffs#di...

🇨🇭Switzerland znerol

znerol made their first commit to this issue’s fork.

🇨🇭Switzerland znerol

I removed the BC for custom $op parameters. Instead _user_mail_notify now maps the $op to the correct method.

One thing to note is that this also changes the return value. Before, FALSE was returned on error and NULL when mail sending was suppressed (either by config or by a hook). The new methods return FALSE in both cases.

🇨🇭Switzerland znerol

From the IS:

Don't re-use user_mail_notify or mutate the email on a cloned version of the user in case someone calls ::save() on it

I think this would be straight forward to implement after 📌 Extract _user_mail_notify() into a user MailHandler Active .

🇨🇭Switzerland znerol

Issue 🐛 Use email verification when changing user email addresses Needs work seeks to add two new operations. It would be good to land this and the other issue in the same release.

🇨🇭Switzerland znerol

Interesting observations in 🐛 _user_mail_notify return error when email sending is aborted Active regarding the _user_mail_notify() return value.

🇨🇭Switzerland znerol

📌 Improve documentation of _user_mail_notify Postponed regrettably ended up in a bike-shed discussion on whether it is necessary to update coding standards before committing the docs improvement. The MR in Use an enum for user email notification types Needs work tries to change every single occurrence of an $op like string regardless of the context. By implementing the proposal from #2 (individual methods on a MailHandlerInterface), it is possible to circumvent both of these pitfalls.

The MR here turns the $op argument into an internal implementation detail. It isn't API anymore and therefore may remain a string.

#3463923-11: Improve documentation of _user_mail_notify points out an interesting angle:

I must say I don't particularly like this, the enum would be much nicer. Regardless, I think it's possible (even if ill-advised) to alter or extend the available options, so this might be needlessly restrictive.

In Drupal 7 it was indeed very easy to extend the list of possible operations via a contrib or custom code. Simply by adding a pair of variable_set() calls in an .install file ensuring that user_mail_OP_subject and user_mail_OP_body contain some meaningful text. I haven't tried it, but it still might be possible to do something like this with a smart combination of form alters and config schema hacks.

This practice should therefore be deprecated properly.

🇨🇭Switzerland znerol

This hasn't sparked any interest and is not being worked on at the moment.

🇨🇭Switzerland znerol

FWIW I would put this issue in the "Mail building Components" section in the roadmap.

I'm okay with that, just do it.

🇨🇭Switzerland znerol

I added that to 🌱 Mailer module roadmap: the path to beta and stable Active . It seems straight forward and actionable.

🇨🇭Switzerland znerol

We decided against the config + plugin approach in our last meeting. Due to the risks of config going missing and due to the fact that recipes do not encourage installing default config.

An issue with the current architecture is that there's a bidirectional dependency between the plugin and the config entity, as it's passed to render. If you compare with block plugins for example, they don't know about block.module block config entities and can be used in other contexts as well. Similar for field formatters, which are also used by views. That's what $configuration is for, the config entity is responsible for storing and passing that in.

That is crucial and it has bothered me a lot. I think I'll try to decouple the config from the plugin and then use that to branch off for coming experiments.

🇨🇭Switzerland znerol

From #3534136-21: Requirements and strategy for Core Mailer by @berdir:

Catching up on the discussion and merge requests. Long comment incoming...

I don't think LOC is the primary factor to consider for this. Also because the current examples are way more complicated than they need to be due to feature flags, the experimental module and BC. The actual implementation would be a lot less.

That said, I do agree that the config entity approach seems complex. Yes, config entity + plugin are a very common pattern, but sometimes also overused. This feels similar to action plugins and config entities, which are often pretty awkward as it's typically a 1:1 relationship and tedious for modules to maintain the config. In general, code that is hardcoding specific config entity ID's is a bit of a code smell to me. As @adamps said, it's unclear what would happen if that config is missing. For example now with recipes, which pretty much *encourages* installing modules without their config ( I don't really agree with that, but that's another topic). Deleting the password reset template

Naming: I think the template part is misleading. That makes me think of twig templates, HTML and customizing their output. Looking at DSM, it uses build() instead of render(), which makes a lot more sense to me. We build an Email object, which might or might not involve rendering something or using templates, but it's not the main thing. It's also similar to terminology used in simplenews.

An issue with the current architecture is that there's a bidirectional dependency between the plugin and the config entity, as it's passed to render. If you compare with block plugins for example, they don't know about block.module block config entities and can be used in other contexts as well. Similar for field formatters, which are also used by views. That's what $configuration is for, the config entity is responsible for storing and passing that in.

Now, there are cases where a fully implemented config entity + plugin could be useful. For example with user, the plugin would be provide a subject and body setting which would make it self-contained instead of accessing global config (which currently bypasses your attempt at making not just mails but also their data and arguments documented and discoverable). and status is built-in, so the code could check that. But it would mean that the UI would move from user settings to generic mail configuration, which is likely harder to find. It's also not as flexible as you'd be used to from plugins. you can't make up your own notifications, you can only enable/disable and edit the specific settings of them. In the end, pretty limited benefit over the current config and some drawbacks.

contact, update are examples that don't really benefit from there being a config entity, and that's going to be a lot of contrib and custom examples. That's combined with the "problem" that the mail rendering/preparing sits entirely on top of the actual mail send API and is there fore optional. People looking at this and trying to send mails will just directly create the Email object and send it directly. And if they do that, we lose the ability to use a mail theme because rendering of stuff possibly already happened. I think we should make it as easy as possible to use the mail API without losing features like language. And then optional features on top. If you look at commerce_mail(), they essentially bypass the whole thing by just passing in the body and rendering it there. I wonder if we should actually allow to use a callback based approach for those minimal cases, similar to what renderInContext() essentially already is.

I'm not sure how important discoverability and reusability of mail "templates" really is, it might in some cases even be undesired. We recently discussed that ECA explicitly doesn't support those special user password reset mails, to not allow to send them out in the wrong context, that could even be a security issue.

On the other side of the spectrum, use cases that actually have generic and complex use cases around mails might struggle to fit into this architecture. Simplenews has multiple things that make up a newsletter, specifically a entity that sent to a subscriber and there's a lot of rendering and logic going on that should happen within the mail render context. And ECA would need to redefine its mails that sit in its configuration or have a super generic one that would almost become recursive.

I think I get the intent with the types and data, but that really feels complex to use. the user confirm thing requires a data type plugin, a definition class, it's passed in as a raw value, converted to typed data and then used. It values (theoretical) reusability over DX, despite all that complexity, what you work with when writing the code is an array with arbitrary keys. I see that DSM has the concept of mail params on the object. That's still keys and not types that can be enforced, but I think that's an improvement over the single magic mixed type that this has. With mulitple params, we can still document them and their type.

I've even been thinking about going entirely with plain value objects implementing a basic interface, but haven't fully thought that through. But overall I'd clearly recommend keeping the actual mail builder interface/API as plain and un-opinionated as possible and start with that.

🇨🇭Switzerland znerol

I think the dates in #12 should be 2015 rather than 2026?

Thanks, fixed.

Would a service locator be an option here instead of the lazy services pass? We've been switching to those in 📌 Replace lazy service proxy generation with service closures Active : Remove lazy declaration and proxy class for cron and use service closure instead and similar issues.

I haven't looked into service locators. The cited issue is using service closures (same tech as the PR). Lazy services are not part of the fix (the dysfunctional lazy flagging is removed in this PR).

As for the question whether it would be possible to do this without a custom service pass: I do not yet know. The service pass has two responsibilities: Assemble every service tagged with http_middleware into a stack. This is done by prepending a reference to the decorated service to the arguments array of the decorator. The second responsibility is to inject all discovered middlewares into the StackedHttpKernel. This is required to propagate the terminate call. Maybe the symfony container provides all the necessary tooling to realize that. But even then, it would be necessary to add some kind of BC pass to move old declarations (tagged with http_middleware to their spot in the stack. I'd be interested to explore. Not sure whether this should be done in this issue or in a follow-up.

🇨🇭Switzerland znerol

The failure of UncaughtExceptionTest::testLoggerException() is interesting. This test examines the PHP error.log in order to check whether exceptions get logged when the normal logger failed.

https://git.drupalcode.org/issue/drupal-3538294/-/pipelines/560646/test_...

Drupal\FunctionalTests\Bootstrap\UncaughtExceptionTest::testLoggerException
The error + the error that the logging service is broken has been written to the error log.
Failed asserting that actual size 12 matches expected size 10.

core/tests/Drupal/FunctionalTests/Bootstrap/UncaughtExceptionTest.php:251

I reproduced it locally and found that NegotiationMiddleware appears in the stack trace with the patch, but doesn't on the 11.x branch. The reason is that MonkeysInTheControlRoom middleware is registered with the same priority. But since PriorityTaggedServiceTrait::findAndSortTaggedServices() uses a different method of sorting, the middlewares swapped their location in the stack.

Bumped the monkey middleware to an appropriate priority to solve the test fail (404).

🇨🇭Switzerland znerol

Drupal CMS baseline:

% composer create-project --no-interaction "drupal/cms:^1.2.x-dev" cms-dev
# Enable some recipes. I had to disable case management and project management due to field storage issues..
# Disable css/js aggregation (not compatible with php built-in webbrowser)
# Rebuild container, clear caches, warm cache
% ab -c 1 -n 500 http://localhost:8888/ > ./cms-dev-baseline.txt
Requests per second:    29.76 [#/sec] (mean)
Time per request:       33.597 [ms] (mean)
Time per request:       33.597 [ms] (mean, across all concurrent requests)

Drupal CMS + patch:

% patch -p1 < /tmp/perf.patch
% # Rebuild container, clear caches, warm cache
% ab -c 1 -n 500 http://localhost:8888/ > ./cms-dev-patched.txt
Requests per second:    44.32 [#/sec] (mean)
Time per request:       22.563 [ms] (mean)
Time per request:       22.563 [ms] (mean, across all concurrent requests)
🇨🇭Switzerland znerol

Performance (Drupal core standard install)

Software:

% uname -v
#1 SMP PREEMPT_DYNAMIC Debian 6.1.140-1 (2025-05-22)

% php -v
PHP 8.3.23 (cli) (built: Jul  3 2025 16:20:45) (NTS)
Copyright (c) The PHP Group
Zend Engine v4.3.23, Copyright (c) Zend Technologies
    with Zend OPcache v8.3.23, Copyright (c), by Zend Technologies
    with Xdebug v3.4.4, Copyright (c) 2002-2025, by Derick Rethans

% podman ps
CONTAINER ID  IMAGE                          COMMAND     CREATED        STATUS            PORTS       NAMES
2967cbf5773c  docker.io/library/postgres:16  postgres    4 minutes ago  Up 4 minutes ago              drupal_memory

Webserver:

% export PHP_CLI_SERVER_WORKERS=4
% php -S localhost:8888

Core baseline:

% composer create-project --no-interaction "drupal/recommended-project:^11.x-dev" core-dev
# Install core standard profile
# Disable css/js aggregation (not compatible with php built-in webbrowser)
# Add node/1
# Rebuild container, clear caches, warm cache
% ab -c 1 -n 500 http://localhost:8888/node/1 > ./core-dev-baseline.txt
Requests per second:    44.61 [#/sec] (mean)
Time per request:       22.415 [ms] (mean)
Time per request:       22.415 [ms] (mean, across all concurrent requests)

Core + patch:

% patch -p1 < /tmp/perf.patch
% # Rebuild container, clear caches, warm cache
% ab -c 1 -n 500 http://localhost:8888/node/1 > ./core-dev-patched.txt
Requests per second:    55.61 [#/sec] (mean)
Time per request:       17.984 [ms] (mean)
Time per request:       17.984 [ms] (mean, across all concurrent requests)
🇨🇭Switzerland znerol

#2408371: Proxies of module interfaces don't work introduced ProxyServicesPass. But that runs before StackedKernelPass. So the lazy flag added StackedKernelPass to middleware definitions on top of the first responder has no effect.

I tried to move the StackedKernelPass before ProxyServicesPass.

diff --git a/core/lib/Drupal/Core/CoreServiceProvider.php b/core/lib/Drupal/Core/CoreServiceProvider.php
index cd9766c14b5..26cab2964e6 100644
--- a/core/lib/Drupal/Core/CoreServiceProvider.php
+++ b/core/lib/Drupal/Core/CoreServiceProvider.php
@@ -73,14 +73,14 @@ public function register(ContainerBuilder $container) {

     $container->addCompilerPass(new SuperUserAccessPolicyPass());

+    $container->addCompilerPass(new StackedKernelPass());
+
     $container->addCompilerPass(new ProxyServicesPass());

     $container->addCompilerPass(new BackendCompilerPass());

     $container->addCompilerPass(new CorsCompilerPass());

-    $container->addCompilerPass(new StackedKernelPass());
-
     $container->addCompilerPass(new StackedSessionHandlerPass());

     $container->addCompilerPass(new MainContentRenderersPass());

But ProxyServicesPass doesn't like that:

[warning] Missing proxy class 'Symfony\Component\ProxyClass\HttpKernel\HttpKernel' for lazy service 'http_kernel.basic'.
Use the following command to generate the proxy class:
  php core/scripts/generate-proxy-class.php 'Symfony\Component\HttpKernel\HttpKernel' "[namespace_root_path]"

ProxyServicesPass.php:63

Besides the HttpKernel, the following middleware classes are reported:

  • Drupal\Core\ProxyClass\StackMiddleware\ContentLength
  • Drupal\Core\ProxyClass\StackMiddleware\KernelPreHandle
  • Drupal\Core\ProxyClass\StackMiddleware\Session
  • Drupal\big_pipe\ProxyClass\StackMiddleware\ContentLength

There are for sure more missing in contrib and custom code. Thus, trying to fix the lazyness is not an option I guess.

🇨🇭Switzerland znerol

Went through an epic git bisect only to find that this broke just two months after it was introduced. Everything happened before 8.0 was even released.

Lazy middlewares and responder tag introduced in: #2473113-47: All stack middlewares are constructed at the same time even for cached pages commit date: 2025-04-30

Lazy middlewares and responder tag broken by: #2408371-83: Proxies of module interfaces don't work commit date: 2025-06-30

🇨🇭Switzerland znerol

Thank a lot @berdir for the review.

🇨🇭Switzerland znerol

And if it doesn't, should the code setting that up be removed?

Yes. In a follow-up.

🇨🇭Switzerland znerol

I do not know what happened to lazy services since #2473113: All stack middlewares are constructed at the same time even for cached pages . They are being removed gradually anyway, so didn't bother to look.

🇨🇭Switzerland znerol

Setting to NR for early feedback. This still needs tests for the deprecation in the page cache middleware. I think we should deprecate the responder tag attribute as well. This could be done in a follow-up though.

🇨🇭Switzerland znerol

Added a draft MR. This prevents the event dispatcher from being instantiated when a page is delivered from the cache.

🇨🇭Switzerland znerol

Not on cached pages. The performance regression exists on every page if you have enough listeners.

Then please submit your PR as a separate issue.

🇨🇭Switzerland znerol

a performance regression is present since 10.3 for any site with a large number of listeners regardless of cached pages.

The performance regression was reported for 11.1 and is clearly measurable on 11.1 on cached responses. Is there any evidence that 10.3 suffered a performance regression on cached pages of the same scale?

🇨🇭Switzerland znerol

I guess this could be solved with a service closure for the $http_kernel argument in PageCache middleware.

🇨🇭Switzerland znerol

Event dispatcher shouldn't be instantiated for cached pages at all. Filed: 🐛 Regression: All stack middlewares are constructed at the same time even for cached pages Active .

🇨🇭Switzerland znerol

Thinking the approach from #37 a bit further. This adds a requirements check to better illustrate the situation.

Some people still want to disable the Password Compatibility module (after switching to Argon2id). Thus, I guess that doesn't need to be moved back to core.

🇨🇭Switzerland znerol

Update the steps to reproduce and did manual testing for the fix.

There are two things which are standing in the way for an automated test here:

  1. The repro requires the SMTP transport (because that is the only built-in transport which attempts to log anything during service destruction). The test systems tries very hard to make sure that no email is sent during automated tests.
  2. The error occurs extremely late in the request-response cycle. It errors when the response is already sent to the simpletest parent site, so we cannot use response headers to communicate it. And we cannot use the database to communicate back the result, because the database connection has been closed already as well. The only thing which could work would be to attempt to search the webserevr / php error log.

Attempting to write an end-to-end test for this case would most probably result in another complex, hard to debug and potentially random failing edge case test.

We are dealing with an experimental plugin here. I propose to either fix it with the proposed one liner or just close the issue as a won't fix.

🇨🇭Switzerland znerol

Thanks! You could remove the public static create method and adapt the constructor as well. This class is explicitly experimental and marked @internal. No need to maintain backwards compatibility here.

🇨🇭Switzerland znerol

znerol changed the visibility of the branch 3420372-core-symfony-mailer to hidden.

🇨🇭Switzerland znerol

Added a new MR which builds on top of the length limit approach: When creating a new password hash which is unsuitable for bcrypt, use phpass as a fallback.

That could be accompanied by a requirement check: If bcrypt is in use, then recommend to either enable phpass or switch to argon2.

🇨🇭Switzerland znerol

Nextcloud is using argon2 since a long time. It appears that they introduced it around NC 14.0.2 (2018).

There are reports on nextcloud forums about login problems after server migrations though. But it looks like people are able to figure out what is causing them.

🇨🇭Switzerland znerol

Argon2 support in Fedora 40 is available via libsodium:

# dnf install php-cli php-sodium

Argon2 support in CentOS 9 is available via epel and libsodium:

# dnf install epel-release
# dnf install php-cli php-sodium
🇨🇭Switzerland znerol

It seems to me that argon2 support is widely available in PHP distro packages. I haven't found any distro which is shipping PHP without argon2 support lately.

Notable exceptions: Fedora 40 (and older), CentOS 9 (and older) https://github.com/znerol-scratch/php-argon2-survey

🇨🇭Switzerland znerol

It seems to me that argon2 support is widely available in PHP distro packages. I haven't found any distro which isn't shipping PHP without argon2 support lately.

I guess the reason why PHP did not switch to argon2 for password hashing by default is because they still are not sure which implementation they want to use. They have compile time support for argon2 via libargon2, via libsodium and recently they added argon2 via openssl (PHP 8.4).

Production build 0.71.5 2024