Skip to content

Commit df83d83

Browse files
committed
Allow to enable/disable header replacement
1 parent 325ee02 commit df83d83

File tree

5 files changed

+130
-1
lines changed

5 files changed

+130
-1
lines changed

guide/index.rst

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,41 @@ Also, you can use constants from the **ResponseHeader** class:
364364
$response->setHeader(ResponseHeader::CONTENT_TYPE, 'text/xml'); // static
365365
$response->setContentType('text/xml'); // static
366366
367+
Repeated Header Names
368+
^^^^^^^^^^^^^^^^^^^^^
369+
370+
The HTTP response allows some headers to be repeated, such as the
371+
**X-Robot-Tag** header.
372+
373+
See the example below setting the header and appending one more:
374+
375+
.. code-block:: php
376+
377+
$response->setHeader('X-Robots-Tag', 'nofollow')
378+
->appendHeader('X-Robots-Tag', 'googlebot: noindex')
379+
380+
Then the response will send multiple headers with the same name:
381+
382+
.. code-block:: http
383+
384+
X-Robots-Tag: nofollow
385+
X-Robots-Tag: googlebot: noindex
386+
387+
If you need to replace the headers, call the ``setReplaceHeaders`` method before
388+
sending the response:
389+
390+
.. code-block:: php
391+
392+
$response->setReplaceHeaders();
393+
$response->setHeader('X-Robots-Tag', 'nofollow')
394+
->appendHeader('X-Robots-Tag', 'googlebot: noindex')
395+
396+
Then the headers will be replaced. And only the last one is sent in the response:
397+
398+
.. code-block:: http
399+
400+
X-Robots-Tag: googlebot: noindex
401+
367402
Response Body
368403
#############
369404

src/Debug/HTTPCollector.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,9 @@ protected function renderResponse() : string
243243
<?php
244244
endif;
245245
echo $this->renderHeadersTable($this->response->getHeaderLines());
246+
if ($this->response->isReplacingHeaders()) {
247+
echo '<p><small>* Note that the Response is replacing headers.</small></p>';
248+
}
246249
echo '<p><small>* Note that some headers can be set outside the Response';
247250
echo ' class, for example by the session or the server.';
248251
echo ' So they don\'t appear here.</small></p>';

src/Response.php

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ class Response extends Message implements ResponseInterface
4949
protected HTTPCollector $debugCollector;
5050
protected CSP $csp;
5151
protected CSP $cspReportOnly;
52+
protected bool $replaceHeaders = false;
5253

5354
/**
5455
* Response constructor.
@@ -524,13 +525,44 @@ protected function sendHeaders() : void
524525
}
525526
$this->negotiateCsp();
526527
$code = $this->getStatusCode();
528+
$replace = $this->isReplacingHeaders();
527529
\header($this->getStartLine(), true, $code);
528530
foreach ($this->getHeaderLines() as $line) {
529-
\header($line, true, $code);
531+
\header($line, $replace, $code);
530532
}
531533
$this->headersSent = true;
532534
}
533535

536+
/**
537+
* The replace parameter indicates whether the header should replace a
538+
* previous similar header, or add a next header of the same type.
539+
* By default, it will replace, but if you pass in false as the first
540+
* argument you can force multiple headers of the same type.
541+
*
542+
* @param bool $replace
543+
*
544+
* @see Response::sendHeaders()
545+
*
546+
* @return static
547+
*/
548+
public function setReplaceHeaders(bool $replace = true) : static
549+
{
550+
$this->replaceHeaders = $replace;
551+
return $this;
552+
}
553+
554+
/**
555+
* Tells if headers are being replaced.
556+
*
557+
* @see Response::setReplaceHeaders()
558+
*
559+
* @return bool
560+
*/
561+
public function isReplacingHeaders() : bool
562+
{
563+
return $this->replaceHeaders;
564+
}
565+
534566
/**
535567
* Set the Content-Security-Policy and Content-Security-Policy-Report-Only
536568
* headers if the CSP classes are set and the response has not downloads.

tests/Debug/HTTPCollectorTest.php

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,4 +213,29 @@ public function testGetActivities() : void
213213
'end',
214214
], \array_keys($this->collector->getActivities()[0])); // @phpstan-ignore-line
215215
}
216+
217+
public function testResponseReplaceHeadersDisabled() : void
218+
{
219+
$response = $this->prepare();
220+
\ob_start();
221+
$response->send();
222+
\ob_end_clean();
223+
self::assertStringNotContainsString(
224+
'Note that the Response is replacing headers',
225+
$this->collector->getContents()
226+
);
227+
}
228+
229+
public function testResponseReplaceHeadersEnabled() : void
230+
{
231+
$response = $this->prepare();
232+
$response->setReplaceHeaders();
233+
\ob_start();
234+
$response->send();
235+
\ob_end_clean();
236+
self::assertStringContainsString(
237+
'Note that the Response is replacing headers',
238+
$this->collector->getContents()
239+
);
240+
}
216241
}

tests/ResponseTest.php

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,40 @@ public function sendHeaders() : void
343343
$response->sendHeaders();
344344
}
345345

346+
public function testReplaceHeadersDisabled() : void
347+
{
348+
self::assertFalse($this->response->isReplacingHeaders());
349+
$this->response
350+
->setHeader('Date', 'foo')
351+
->appendHeader('Date', 'bar')
352+
->appendHeader('Date', 'baz');
353+
\ob_start();
354+
$this->response->send();
355+
\ob_get_clean();
356+
$headers = xdebug_get_headers();
357+
self::assertContains('Date: foo', $headers);
358+
self::assertContains('Date: bar', $headers);
359+
self::assertContains('Date: baz', $headers);
360+
}
361+
362+
public function testReplaceHeadersEnabled() : void
363+
{
364+
self::assertFalse($this->response->isReplacingHeaders());
365+
$this->response->setReplaceHeaders();
366+
self::assertTrue($this->response->isReplacingHeaders());
367+
$this->response
368+
->setHeader('Date', 'foo')
369+
->appendHeader('Date', 'bar')
370+
->appendHeader('Date', 'baz');
371+
\ob_start();
372+
$this->response->send();
373+
\ob_get_clean();
374+
$headers = xdebug_get_headers();
375+
self::assertNotContains('Date: foo', $headers);
376+
self::assertNotContains('Date: bar', $headers);
377+
self::assertContains('Date: baz', $headers);
378+
}
379+
346380
public function testInvalidStatusCode() : void
347381
{
348382
$this->expectException(\InvalidArgumentException::class);

0 commit comments

Comments
 (0)