Skip to content

Commit

Permalink
replace App::$_sentHeaders with PSR7 response data
Browse files Browse the repository at this point in the history
  • Loading branch information
mvorisek committed Feb 27, 2023
1 parent 91e12db commit 63706b1
Show file tree
Hide file tree
Showing 3 changed files with 48 additions and 92 deletions.
130 changes: 46 additions & 84 deletions src/App.php
Original file line number Diff line number Diff line change
Expand Up @@ -107,11 +107,6 @@ class App

private ResponseInterface $response;

/** @var array<string, string> Extra HTTP headers to send on exit. */
protected array $responseHeaders = [
'cache-control' => 'no-store', // disable caching by default
];

/** @var array<string, View> Modal view that need to be rendered using json output. */
private $portals = [];

Expand Down Expand Up @@ -170,6 +165,9 @@ public function __construct(array $defaults = [])
$this->response = new Response();
}

// disable caching by default
$this->setResponseHeader('cache-control', 'no-store');

$this->setApp($this);

$this->setDefaults($defaults);
Expand Down Expand Up @@ -389,17 +387,24 @@ public function setResponseStatusCode(int $statusCode): self
/**
* @return $this
*/
public function setResponseHeader(string $name, string $value): self
public function setResponseHeader(string $name, ?string $value): self
{
if ($value !== '') {
$this->responseHeaders[$name] = $value;
if ($value === null) {
$this->response = $this->response->withoutHeader($name);
} else {
unset($this->responseHeaders[$name]);
$name = preg_replace_callback('~(?<![a-zA-Z])[a-z]~', function ($matches) {
return strtoupper($matches[0]);
}, $name);

$this->response = $this->response->withHeader($name, $value);
}

return $this;
}

/**
* @param array<string, string|null> $headers
*/
private function setResponseHeaders(array $headers = []): void
{
foreach ($headers as $name => $value) {
Expand All @@ -419,23 +424,19 @@ private function setResponseHeaders(array $headers = []): void
*/
public function terminate($output = '', array $headers = []): void
{
if (!isset($headers['content-type'])) {
if (!isset($this->responseHeaders['content-type'])) {
throw new Exception('Content type must be always set');
}
$this->setResponseHeaders($headers);

$headers['content-type'] = $this->responseHeaders['content-type'];
$type = preg_replace('~;.*~', '', strtolower($this->response->getHeaderLine('content-type'))); // in LC without charset
if ($type === '') {
throw new Exception('Content type must be always set');
}

$type = preg_replace('~;.*~', '', strtolower($headers['content-type'])); // in LC without charset

if ($type === 'application/json') {
if (is_string($output)) {
$output = $this->decodeJson($output);
}
$output['portals'] = $this->getRenderedPortals();

$this->setResponseHeaders($headers);
$this->outputResponseJson($output);
} elseif (isset($_GET['__atk_tab']) && $type === 'text/html') {
// ugly hack for TABS
Expand All @@ -456,13 +457,11 @@ public function terminate($output = '', array $headers = []): void
$output = $this->getTag('script', [], '$(function () {' . $remove_function . $output['atkjs'] . '});')
. $output['html'];

$this->setResponseHeaders($headers);
$this->outputResponseHtml($output, $headers);
$this->outputResponseHtml($output);
} elseif ($type === 'text/html') {
$this->setResponseHeaders($headers);
$this->outputResponseHtml($output, $headers);
$this->outputResponseHtml($output);
} else {
$this->outputResponse($output, $headers);
$this->outputResponse($output);
}

$this->runCalled = true; // prevent shutdown function from triggering
Expand Down Expand Up @@ -1110,24 +1109,36 @@ protected function outputResponseUnsafe(string $data): void
{
http_response_code($this->response->getStatusCode());

$isCli = \PHP_SAPI === 'cli'; // for phpunit

/* if (count($headersNew) > 0 && headers_sent() && !$isCli) {
$lateError = new LateOutputError('Headers already sent, more headers cannot be set at this stage');
if ($this->catchExceptions) {
$this->caughtException($lateError);
$this->outputLateOutputError($lateError);
}
throw $lateError;
} */

if (!headers_sent() || $isCli) {
foreach ($this->response->getHeaders() as $name => $values) {
foreach ($values as $value) {
if (!$isCli) {
header($name . ': ' . $value, false);
}
}
}
}

echo $data;
}

/** @var array<string, string> */
private static array $_sentHeaders = [];

/**
* Output Response to the client.
*
* @param array<string, string> $headers
*/
protected function outputResponse(string $data, array $headers): void
protected function outputResponse(string $data): void
{
$headersAll = array_merge($this->responseHeaders, $headers);
unset($headers);
$headersNew = array_diff_assoc($headersAll, self::$_sentHeaders);
unset($headersAll);

foreach (ob_get_status(true) as $status) {
if ($status['buffer_used'] !== 0) {
$lateError = new LateOutputError('Unexpected output detected');
Expand All @@ -1140,34 +1151,6 @@ protected function outputResponse(string $data, array $headers): void
}
}

$isCli = \PHP_SAPI === 'cli'; // for phpunit

if (count($headersNew) > 0 && headers_sent() && !$isCli) {
$lateError = new LateOutputError('Headers already sent, more headers cannot be set at this stage');
if ($this->catchExceptions) {
$this->caughtException($lateError);
$this->outputLateOutputError($lateError);
}

throw $lateError;
}

foreach ($headersNew as $k => $v) {
self::$_sentHeaders[$k] = $v;
}

if (!headers_sent() || $isCli) {
foreach ($headersNew as $k => $v) {
if (!$isCli) {
$kCamelCase = preg_replace_callback('~(?<![a-zA-Z])[a-z]~', function ($matches) {
return strtoupper($matches[0]);
}, $k);

header($kCamelCase . ': ' . $v);
}
}
}

$this->outputResponseUnsafe($data);
}

Expand All @@ -1177,31 +1160,10 @@ protected function outputResponse(string $data, array $headers): void
protected function outputLateOutputError(LateOutputError $exception): void
{
$this->setResponseStatusCode(500);
$this->setResponseHeader('content-type', 'text/plain');

$plainTextMessage = "\n" . '!! FATAL UI ERROR: ' . $exception->getMessage() . ' !!' . "\n";

$headersAll = ['content-type' => 'text/plain'];
$headersNew = array_diff_assoc($headersAll, self::$_sentHeaders);
unset($headersAll);

foreach ($headersNew as $k => $v) {
self::$_sentHeaders[$k] = $v;
}

$isCli = \PHP_SAPI === 'cli'; // for phpunit

if (!headers_sent() || $isCli) {
foreach ($headersNew as $k => $v) {
if (!$isCli) {
$kCamelCase = preg_replace_callback('~(?<![a-zA-Z])[a-z]~', function ($matches) {
return strtoupper($matches[0]);
}, $k);

header($kCamelCase . ': ' . $v);
}
}
}

$this->outputResponseUnsafe($plainTextMessage);

$this->runCalled = true; // prevent shutdown function from triggering
Expand All @@ -1221,7 +1183,7 @@ private function outputResponseHtml(string $data): void
/**
* Output JSON response to the client.
*
* @param string|array $data
* @param string|array $data
*/
private function outputResponseJson($data): void
{
Expand Down
2 changes: 1 addition & 1 deletion tests/CallbackTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ class AppMock extends App
/**
* Overriden to allow multiple App::run() calls, prevent sending headers when headers are already sent.
*/
protected function outputResponse(string $data, array $headers): void
protected function outputResponse(string $data): void
{
echo $data;
}
Expand Down
8 changes: 1 addition & 7 deletions tests/DemosTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -117,10 +117,6 @@ protected function setSuperglobalsFromRequest(RequestInterface $request): void
foreach ($queryArr as $k => $v) {
$_POST[$k] = $v;
}

\Closure::bind(function () {
App::$_sentHeaders = [];
}, null, App::class)();
}

protected function resetSuperglobals(): void
Expand Down Expand Up @@ -194,12 +190,10 @@ protected function getClient(): Client
$this->resetSuperglobals();
}

$headers = \Closure::bind(fn () => App::$_sentHeaders, null, App::class)();

// Attach a response to the easy handle with the parsed headers.
$response = new Response(
$app->getResponse()->getStatusCode(), // @phpstan-ignore-line
$headers,
$app->getResponse()->getHeaders(), // @phpstan-ignore-line
class_exists(Utils::class) ? Utils::streamFor($body) : \GuzzleHttp\Psr7\stream_for($body), // @phpstan-ignore-line Utils class present since guzzlehttp/psr7 v1.7
'1.0'
);
Expand Down

0 comments on commit 63706b1

Please sign in to comment.