Problem/Motivation

This is a child issue and a blocker of #2352155: Remove HtmlFragment/HtmlPage.

Part of untangling the (page) rendering pipeline is removing edge cases, and hence being more strict, so that the DX is better (i.e. developers getting useful exceptions).

The prime example is that for historical reasons, we still allow strings to be returned by _content controllers.

Proposed resolution

Don't allow strings to be returned by _content controllers, only allow render arrays to be returned. Since only tests still return strings, this does not worsen the DX! In fact, it improves it, by making things more strict.

Remaining tasks

Review.

User interface changes

None.

API changes

Not really any. Strings are no longer valid return values for _content controllers, but that already was enforced by convention; the only things still using that are tests!

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

Wim Leers’s picture

Status: Active » Needs review
FileSize
16.67 KB
moshe weitzman’s picture

Will some other issue add the exception when a controller returns a string?

larowlan’s picture

Yes I was expecting to see the exception too

Wim Leers’s picture

Assigned: Unassigned » Wim Leers
FileSize
19.18 KB
1.55 KB

That's because I'm dumb: I failed to create the patch correctly :/

#1 is 1:1 from the main patch.

In this interdiff, you see the changes I intended to make. Typehinting to array provides the necessary feedback. You'll see errors like:

Recoverable fatal error: Argument 1 passed to Drupal\Core\Controller\HtmlControllerBase::createHtmlFragment() must be of the type array, string given […]

(Give it a quick shot by applying the patch, modifying UserController::userPage() to return a string, go to /user, BOOM!)

Status: Needs review » Needs work

The last submitted patch, 4: _content_controllers_no_strings-2363139-4.patch, failed testing.

Wim Leers’s picture

Status: Needs work » Needs review
FileSize
19.09 KB
1.55 KB

Oops. I applied the pattern from the main patch here, but that doesn't fly, because the main patch removes HtmlFragment, which of course isn't the case here.
(It also means that the existing documentation is outrageously wrong.)

So, no more reliance on a typehint, sadly, but an exception instead. In the main patch, typehinting will suffice.

tim.plunkett’s picture

  1. +++ b/core/lib/Drupal/Core/Controller/HtmlControllerBase.php
    @@ -49,20 +49,26 @@ public function __construct(TitleResolverInterface $title_resolver, RenderHtmlRe
    +   * @param array|\Drupal\Core\Page\HtmlFragment $page_content
    ...
         if ($page_content instanceof HtmlFragment || $page_content instanceof Response) {
    

    Should this also contain Response as a potential type?

  2. +++ b/core/lib/Drupal/Core/Controller/HtmlControllerBase.php
    @@ -49,20 +49,26 @@ public function __construct(TitleResolverInterface $title_resolver, RenderHtmlRe
    +   * @throws \LogicException
    ...
    +    if (is_string($page_content)) {
    +      throw new \LogicException('_content controllers may not return strings.');
    

    No reason to hold the patch up, but this is a perfect usage for \InvalidArgumentException

Status: Needs review » Needs work

The last submitted patch, 6: _content_controllers_no_strings-2363139-6.patch, failed testing.

Wim Leers’s picture

Status: Needs work » Needs review
FileSize
19.58 KB
2.15 KB
  1. Yes, of course! Missed that.
  2. Oooohhhh! Hadn't seen that one yet. Thank you! :)

Also fixed that last exception.

moshe weitzman’s picture

+   *
+   * @throws \LogicException

No longer true.

Otherwise looks good.

dawehner’s picture

Component: base system » request processing system

Are we sure there is no documentation at all in Drupal core which documents this?

larowlan’s picture

fixes #10, reviewed again - agree with @moshe - +1 rtbc

dawehner’s picture

FileSize
19.37 KB
2.35 KB

Let's actually provide something helpful.

Wim Leers’s picture

Are we sure there is no documentation at all in Drupal core which documents this?

If there is, I didn't find it.

+++ b/core/lib/Drupal/Core/Controller/HtmlControllerBase.php
@@ -49,7 +49,7 @@ public function __construct(TitleResolverInterface $title_resolver, RenderHtmlRe
+   * @param array|\Drupal\Core\Page\HtmlFragmentInterface|\Symfony\Component\HttpFoundation\Response $page_content

HtmlFragmentInterface is what I wanted to use, but the code specifically checks for instanceof HtmlFragment, not HtmlFragmentInterface, hence I chose to typehint the same way.

… but anyway, it's going to be removed, so it doesn't matter much. I'll roll with it.

Wim Leers’s picture

Any other concerns, or is this RTBC? :)

moshe weitzman’s picture

Status: Needs review » Reviewed & tested by the community

No concerns from me.

alexpott’s picture

Status: Reviewed & tested by the community » Fixed

Committed 961f188 and pushed to 8.0.x. Thanks!

+++ b/core/modules/system/src/Tests/Routing/RouterTest.php
@@ -44,7 +44,7 @@ public function testDefaultController() {
-    // a page inception style.  This test verifies that is not happening.
+    // a page inception style.  This test verifies that is not happening

Unrelated and doesn't fix the code style bug there anyway - just makes it worse. Removed from the patch.

  • alexpott committed 961f188 on 8.0.x
    Issue #2363139 by Wim Leers, dawehner, larowlan: _content controllers...
Wim Leers’s picture

That was probably accidental — it was added by dawehner in #13. Thanks!

joelpittet’s picture

I've written a concern elsewhere but it's puzzling how this is a DX improvement, where before if you're doing a test you could just provide a string which is the end result response to the browser. And now we must wrap it to get it back to a string through a superfluous drupal_render call.

I'm probably missing things, sorry I didn't get a chance to ask before it was committed.

Other than the type consistency what are we gaining from this?

Fabianx’s picture

RTBC + 1 (in retrospective)

Wim Leers’s picture

That's it: consistency. But also: less magic.

If we want developers to be able to just return a string, then the underlying system has to apply magic: detect that it's a string and convert it into a render array.

Both more consistency and less magic improve the DX. The fact that we have zero non-test controllers returning strings shows how rare this is, and hence how few developers will be affected.

moshe weitzman’s picture

I'll add another: discoverability. When you can just return a string, you do so. And then you have less opportunity to learn that should not be theming stuff in your callback, but rather deferring theming via #theme. And then you learn that you should use #attached instead of adding css/js/library globally. And then you learn that you can render cache expensive things, with great control over cache keys, cache tags, expire time, bin, etc.

joelpittet’s picture

Thanks for the responses @Wim Leers and @moshe weitzman, just something I'll need to get used to a bit.

I liked the magic when it gets me from Point A to Point B with little resistance. I relate it to inline templating: it's really not something you should be doing, but for rapid prototyping it will get you there faster, then you can work on splitting up the concerns into the template.html.twig and #theme definitions once you know your idea has wings.

Wim Leers’s picture

Are you really returning just a string that often?

It's a small price to pay for less magic/consistent DX/discoverability, to have the 1% use case of returning a string require returning ['#markup'=> $string] instead.

joelpittet’s picture

@Wim Leers yes although I seem to be in the 1% minority... but it's important to me so I thought I'd speak up.

For workflow: I go with sanity test/minimal viable code: "Can I print something to the browser?" is a good start. Massage it till it does what I want. Then refactor a few times to add things #cache, #attached, #theme and all the other things that require extra files or configuration that are not needed at the beginning "hello world".

return ['#markup'=> $string] feels unnecessary when what my end goal is to

print $string;

(Purposely changed that to print because that is what I did in 6/7 for a blank viewport on hook_menu, though return from the controller is fine).

Wim Leers’s picture

If you really just want a string to be printed, and nothing else, then just do return new Response('string'), which is what Symfony _controllers are intended to return.
The difference is that this will literally just print that string, no HTML, no blocks. But if what you're looking for is "to get something to the browser", then this is what makes most sense in a Symfony world.

joelpittet’s picture

@Wim Leers oh thanks for clarifying the Response() bit I should know that actually, not sure why that didn't cross my mind... I'll likely use that in my little sanity checks:)

I referenced this stackoverflow question before in the other issue discussing this:
http://stackoverflow.com/questions/553936/in-mvc-how-do-i-return-a-strin...

For the likely "Why doesn't it just work like this" type of question I'm expecting. The snippet is MVC.net, although they are similar concepts. new Response() will be Content() but you still can if you want pass a "string" from the controller's action in MVC.net though most likely you'd use a View() in MVC.net just like you'd use ['#theme' => 'my_theme_hook'] here.

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.