Problem/Motivation

The ContentNegotiation class is only used in the NegotiationMiddleware class. It is trivial in size and is not useful anywhere else. Let's simplify by just moving its functionality into the middleware.

Proposed resolution

* Move the getContentType() method from the ContentNegotiation class to NegotiationMiddleware and make it protected.
* Adapt NegotiationMiddleware to call the method on itself instead of on ContentNegotiation.
* Remove the dependency of NegotiationMiddleware on ContentNegotiation.
* Remove ContentNegotiation entirely (including from services.yml)
* Dance.

Remaining tasks

Do it.

User interface changes

none

API changes

nothing significant.

Beta phase evaluation

Reference: https://www.drupal.org/core/beta-changes
Issue category Task because it's just general cleanup
Issue priority Normal because nothing's actual broken. This is mostly just tidiness, with maybe a micro-optimization benefit

Comments

Jaesin’s picture

Status: Active » Needs review
StatusFileSize
new7.6 KB

I created a ContentNegotiationInterface interface and updated accept_header_routing_test to use a custom content negotiator instead of custom middleware. There still isn't web test coverage for this though.

Jaesin’s picture

Issue summary: View changes

Update motivation text to be more precise.

dawehner’s picture

Is there an actual usecase in replacing it? I mean its something you should actually not replace, given how hacky it is already. I'd honestly rather expected people to replace the middleware entirely.

neclimdul’s picture

Seeing that error lazy me would have just extended the class and overrode the methods... Daniel is right though, if you really need to replace the functionality I'd error on replacing the middleware.

+++ /dev/null
@@ -1,47 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains \Drupal\accept_header_routing_test\AcceptHeaderMiddleware.
- */
-
-namespace Drupal\accept_header_routing_test;
-
-use Symfony\Component\HttpFoundation\Request;
-use Symfony\Component\HttpKernel\HttpKernelInterface;
-
-/**
- * Example implementation of accept header based content negotation.
- */
-class AcceptHeaderMiddleware implements HttpKernelInterface {
-
-  /**
-   * Constructs a new AcceptHeaderMiddleware instance.
-   *
-   * @param \Symfony\Component\HttpKernel\HttpKernelInterface $app
-   *   The app.
-   */
-  public function __construct(HttpKernelInterface $app) {
-    $this->app = $app;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function handle(Request $request, $type = self::MASTER_REQUEST, $catch = TRUE) {
-    $mapping = [
-      'application/json' => 'json',
-      'application/hal+json' => 'hal_json',
-      'application/xml' => 'xml',
-      'text/html' => 'html',
-    ];
-
-    $accept = $request->headers->get('Accept') ?: ['text/html'];
-    if (isset($mapping[$accept[0]])) {
-      $request->setRequestFormat($mapping[$accept[0]]);
-    }
-
-    return $this->app->handle($request, $type, $catch);
-  }
-
-}

+++ b/core/modules/system/tests/modules/accept_header_routing_test/src/CustomContentNegotiation.php
@@ -0,0 +1,55 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\accept_header_routing_test\CustomContentNegotiation.
+ */
+
+namespace Drupal\accept_header_routing_test;
+
+use Drupal\Core\ContentNegotiationInterface;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * Provides content negotiation based upon query parameters and the accept header.
+ */
+class CustomContentNegotiation implements ContentNegotiationInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getContentType(Request $request) {
+    // AJAX iframe uploads need special handling, because they contain a JSON
+    // response wrapped in <textarea>.
+    if ($request->get('ajax_iframe_upload', FALSE)) {
+      return 'iframeupload';
+    }
+
+    if ($request->query->has('_format')) {
+      return $request->query->get('_format');
+    }
+
+    // Create accept header map.
+    $type_map = [
+      'application/json' => 'json',
+      'application/hal+json' => 'hal_json',
+      'application/xml' => 'xml',
+      'text/html' => 'html',
+    ];
+
+    // Get the first accept header.
+    $accept = explode(',', $request->headers->get('Accept'));
+
+    // Check to see if the accept header is in out list.
+    if (isset($type_map[$accept[0]])) {
+      return $type_map[$accept[0]];
+    }
+
+    if ($request->isXmlHttpRequest()) {
+      return 'ajax';
+    }
+
+    // Do HTML last so that it always wins.
+    return 'html';
+  }
+}

what is all this?

Jaesin’s picture

That is the replacement for this code that is in the middleware.

+++ /dev/null
@@ -1,47 +0,0 @@
- * Contains \Drupal\accept_header_routing_test\AcceptHeaderMiddleware.
...
-  /**
-   * {@inheritdoc}
-   */
-  public function handle(Request $request, $type = self::MASTER_REQUEST, $catch = TRUE) {
-    $mapping = [
-      'application/json' => 'json',
-      'application/hal+json' => 'hal_json',
-      'application/xml' => 'xml',
-      'text/html' => 'html',
-    ];
-
-    $accept = $request->headers->get('Accept') ?: ['text/html'];
-    if (isset($mapping[$accept[0]])) {
-      $request->setRequestFormat($mapping[$accept[0]]);
-    }
-
-    return $this->app->handle($request, $type, $catch);
-  }

@dawehner: I agree it is hacky and it would be great if content negotiation was pluggable but that has performance considerations. I think this is a compromise that feels less hacky than forcing an override of the middleware.

My use case. I have a requirement for custom REST endpoints that uses a .json extension and .json?_format=json is redundant.

dawehner’s picture

@Jaesin
Do you mind removing the test code changes? I think a real accept header based middleware should be a middleware and not need that additional level of indirection.

Jaesin’s picture

StatusFileSize
new2.8 KB
new3.28 KB

Sure. No Problem.

wim leers’s picture

So, regarding overriding the entire middleware (@dawehner + @neclimdul) versus just overriding the ContentNegotiation service:

  • It can be argued both ways.
  • I think micro performance matters greatly in this case, so I wonder if the reason no interface exists already is to avoid that one additional OpCache retrieval?
  • Services — private or not — can be overridden. And the rule is that any service has an interface, so it can be overridden.
  • Therefore I see two valid solutions:
    1. Allow the http_negotiation.format_negotiator service to be overridden: Add an interface to the service (i.e. the current patch).
    2. Only allow the http_middleware.negotiation middleware service to be overridden: Remove ContentNegotiation's logic and move it into NegotiationMiddleware. It seems they're tightly coupled anyway, and the complexity of ContentNegotiation is so small that it does actually make the overall logic simpler.
Crell’s picture

Re #8: The reason no interface exists is because the ContentNegotiation class was never expected to live this long. :-) It was added in a hacky form 3 years ago and we've tried to kill it about 4 times. It's a tricky bugger. Opcache micro-optimization was never a consideration.

Given how trivial ContentNegotiation is in its current form, I'd be in favor of #8.2: Just kill it finally and move it into the middleware. We can probably just move the getContentType() method itself to the middleware class and be done with it.

dawehner’s picture

Given how trivial ContentNegotiation is in its current form, I'd be in favor of #8.2: Just kill it finally and move it into the middleware. We can probably just move the getContentType() method itself to the middleware class and be done with it.

+1 to do that. For me ContentNegotation is really just about helping the middleware, its kinda a private method.

wim leers’s picture

Issue tags: +Novice, +php-novice

Yep, makes sense :)

wim leers’s picture

Status: Needs review » Needs work
Crell’s picture

Issue summary: View changes

Updating IS accordingly. Agreed that this is a good Novice candidate so giving a precise outline. Also, beta eval.

minnur’s picture

Status: Needs work » Needs review
StatusFileSize
new9.53 KB

I made this update. Please review.

minnur’s picture

StatusFileSize
new10.17 KB

Forgot to add my interdiff.txt file.

Jaesin’s picture

+++ b/core/lib/Drupal/Core/StackMiddleware/NegotiationMiddleware.php
@@ -81,5 +69,32 @@ public function registerFormat($format, $mime_type) {
+  ¶

There is a in space in there that should be removed.

Otherwise, given the consensus, I think this looks good. Thanks for porting the unit test. NegotiationMiddleware unit test coverage could be expanded but that is kinda outside this issue.

tim.plunkett’s picture

This issue needs a new title.

minnur’s picture

StatusFileSize
new9.48 KB
new9.48 KB

@Jaesin: I removed spaces.

dawehner’s picture

Looks good in general.

+++ b/core/lib/Drupal/Core/StackMiddleware/NegotiationMiddleware.php
@@ -81,5 +69,32 @@ public function registerFormat($format, $mime_type) {
+   *   The request object from which to extract the content type.
...
+  public function getContentType(Request $request) {

This no longer needs to be public.

dawehner’s picture

Status: Needs review » Needs work

.

sdstyles’s picture

StatusFileSize
new6.79 KB
new740 bytes
sdstyles’s picture

Status: Needs work » Needs review
dawehner’s picture

+++ b/core/lib/Drupal/Core/StackMiddleware/NegotiationMiddleware.php
@@ -82,7 +82,7 @@ public function registerFormat($format, $mime_type) {
+  private function getContentType(Request $request) {

Let's make it protected

minnur’s picture

StatusFileSize
new7.51 KB
new2.73 KB
dawehner’s picture

Status: Needs review » Reviewed & tested by the community

Thank you

alexpott’s picture

Title: ContentNegotiation should implement an interface. » Remove ContentNegotiation and embed functionality in the middleware
Status: Reviewed & tested by the community » Needs work
Issue tags: +Needs change record

Since this has been around for 3 years I guess we need a CR to tell people to just override the middleware in order to do their own content negotiation.

neclimdul’s picture

+++ b/core/tests/Drupal/Tests/Core/StackMiddleware/NegotiationMiddlewareTest.php
@@ -0,0 +1,19 @@
+class NegotiationMiddlewareTest extends UnitTestCase {
+
+}

???

neclimdul’s picture

StatusFileSize
new4.62 KB
new11.52 KB

NM, I see. They where removed because the method wasn't public. re-adding tests. Also some more tests just because I'm a fan of test coverage.

neclimdul’s picture

Status: Needs work » Needs review

to the testbot. #26 still applies though.

Crell’s picture

  1. +++ b/core/tests/Drupal/Tests/Core/StackMiddleware/NegotiationMiddlewareTest.php
    @@ -0,0 +1,142 @@
    +   * Test that handle correctly hands off to sub application.
    

    Minor nit: handle(). Otherwise I kept reading "handle correctly" to mean that something else handled something correctly, which is totally not what we're saying here. :-)

  2. +++ b/core/tests/Drupal/Tests/Core/StackMiddleware/NegotiationMiddlewareTest.php
    @@ -0,0 +1,142 @@
    +    $request = $this->prophesize('\Symfony\Component\HttpFoundation\Request');
    

    We can use PHP 5.5 style class naming here now! Huzzah!

  3. +++ b/core/tests/Drupal/Tests/Core/StackMiddleware/NegotiationMiddlewareTest.php
    @@ -0,0 +1,142 @@
    +    $request = $this->prophesize('\Symfony\Component\HttpFoundation\Request');
    

    Here too.

dawehner’s picture

Issue tags: -Needs change record

Added one

Jaesin’s picture

StatusFileSize
new11.45 KB

No interdiff because I manually edited the patch.

#30-1 Changed handle to handle()
#30-2 changed ''\Symfony\Component\HttpKernel\HttpKernelInterface'' to HttpKernelInterface::class
#30-2 changed '\Symfony\Component\HttpFoundation\Request' to Request::class (x2)

I am wondering how using ::class performs as opposed to using single quotes.

Crell’s picture

Status: Needs review » Reviewed & tested by the community

Let's do this!

(Side note: ::class is a constant, so it evaluates to the class name at compile time, not runtime. There shouldn't be any difference at runtime between a string literal and a constant, or if so it's so small that you wouldn't even notice.)

Jaesin’s picture

A quick little test:

<?php
namespace Drupal\QtTest;

/**
 * Test Class.
 **/
class TestClass {
	public $counter = 1;
}

$test_string = '';

// Print the start time.
print(time().chr(10));

// Why not.
$test = new TestClass();

// Test no 1. quoted string. 
$start = microtime(TRUE);
for ($i=0; $i < 100000000; $i++) { 
	// Just do something.
	$test->counter +=1;
	$test_string = 'Drupal\test\NSNameSpace\TestClass';
}
$qtResult = microtime(TRUE)-$start;
print('static: '.$test_string.chr(10));
print($qtResult.chr(10));

// Test no. 2 static class.
$start = microtime(TRUE);
for ($i=0; $i < 100000000; $i++) { 
	// Just do something.
	$test->counter +=1;
	$test_string = TestClass::class;
}
$classResult = microtime(TRUE)-$start;
print('::class '.$test_string.chr(10));
print($classResult.chr(10));

?>

Interesting result:

1439313066
static: Drupal\QtTest\TestClass
12.260830879211
::class Drupal\QtTest\TestClass
12.128791809082

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 32: remove-2506533-32.patch, failed testing.

Status: Needs work » Needs review

Crell queued 32: remove-2506533-32.patch for re-testing.

Status: Needs review » Needs work

The last submitted patch, 32: remove-2506533-32.patch, failed testing.

neclimdul’s picture

Can't reproduce. Might be a rouge testbot.

Status: Needs work » Needs review

neclimdul queued 32: remove-2506533-32.patch for re-testing.

neclimdul’s picture

Status: Needs review » Reviewed & tested by the community

yeah, there was some problem with opcode caching something something. back to previous status.

dawehner’s picture

+1 for the patch

alexpott’s picture

Status: Reviewed & tested by the community » Fixed

This makes sense - if you want to swap out the negotiation layer you'll need to swap out http_middleware.negotiation - the http_negotiation.format_negotiator is just noise. This patch is just followup from all the work in this area.

Committed f29f1e2 and pushed to 8.0.x. Thanks!

Thanks for adding the beta evaluation to the issue summary.

  • alexpott committed f29f1e2 on 8.0.x
    Issue #2506533 by Jaesin, minnur, neclimdul, sdstyles, dawehner, Crell:...

Status: Fixed » Closed (fixed)

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

fabianx’s picture

Published the change record.