Skip to content

Commit

Permalink
HeaderCollection rewrite
Browse files Browse the repository at this point in the history
  • Loading branch information
donwilson committed Nov 30, 2023
1 parent b8c16f6 commit 305359f
Show file tree
Hide file tree
Showing 4 changed files with 154 additions and 96 deletions.
3 changes: 3 additions & 0 deletions src/Magnetar/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,9 @@ public function registerCoreContainerAliases(): void {
\Magnetar\Container\Container::class,
\Magnetar\Application::class,
],
'auth' => [
\Magnetar\Auth\AuthManager::class,
],
'cache' => [
\Magnetar\Cache\StoreManager::class,
],
Expand Down
1 change: 0 additions & 1 deletion src/Magnetar/Cache/CacheServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ public function register(): void {
public function provides(): array {
return [
'cache',
//StoreManager::class
];
}
}
197 changes: 126 additions & 71 deletions src/Magnetar/Http/HeaderCollection.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,93 +3,125 @@

namespace Magnetar\Http;

/**
* Represents a collection of HTTP headers
*/
class HeaderCollection {
protected const UPPER = '_ABCDEFGHIJKLMNOPQRSTUVWXYZ';
protected const LOWER = '-abcdefghijklmnopqrstuvwxyz';

/**
* Headers array
* @var array
*/
protected array $headers = [];

/**
* Store response codes for headers that have them
* @var array
*/
protected array $response_codes = [];

/**
* Constructor
*/
public function __construct(
/**
* The headers to add
* @var array
*/
array $headers=[]
) {
foreach($headers as $key => $value) {
$this->add($key, $value);
}
}

/**
* Add header
* @param string $header The header to add
* @param bool|int $replace Whether to replace an existing header with the same name
* @param int|null $response_code The HTTP response code to send
*/
public function add(
string $header_key,
string $header_value='',
string $name,
array|string $value='',
bool|int $replace=true,
int|null $response_code=0
): self {
if('' === ($header_key = $this->sanitizeHeaderKey($header_key))) {
// sanitize
if('' === ($name_sanitized = $this->sanitizeName($name))) {
return $this;
}

$this->headers[] = [
'header' => $header_key,
'value' => $this->sanitizeHeaderValue($header_value),
'replace' => $replace,
'response_code' => $response_code
];
// check if header already exists, if so and we're not replacing, don't add
if(isset($this->headers[ $name_sanitized ]) && !$replace) {
return $this;
}

// @TODO add validation support
// @TODO add sanitization (trim, lowercase header name, etc)
// @TODO utilize replace logic in add()
// start building header
$this->headers[ $name_sanitized ] = [];

return $this;
}

/**
* Send all headers
* @return void
*/
public function send(): void {
foreach($this->headers as $header) {
header($header['header'] .':'. $header['value'], $header['replace'], $header['response_code']);
if(is_array($value)) {
// add each value
foreach($value as $header_value) {
$this->headers[ $name_sanitized ][] = $header_value;
}
} else {
// set single value as array
$this->headers[ $name_sanitized ] = [$value];
}

if(0 !== $response_code) {
// edge-case: one could provide different response codes for each added header
// but only the last one (that isn't the default value) is stored for each header
$this->response_codes[ $header_sanitized ] = $response_code;
}

return $this;
}

/**
* Get all headers
* @param string|null $name If provided, only return headers with this name
* @return array
*/
public function all(): array {
public function all(string|null $name=null): array {
if(null !== $name) {
// return only headers with this name
return $this->headers[ $this->sanitizeName($name) ] ?? [];
}

return $this->headers;
}

/**
* Sanitize a header key (everything before the header's first colon)
* @param string $header_key
* @return string
*/
public function sanitizeHeaderKey(string $header_key): string {
return strtolower(trim($header_key));
}

/**
* Sanitize a header value (everything after the header's first colon)
* @param string $header_value
* @return string
*/
public function sanitizeHeaderValue(string $header_value): string {
return trim($header_value);
}

/**
* Get a header by name
* @param string $name The header name
* @return string|null
*/
public function get(string $name): ?string {
// sanitize
if('' === ($name = $this->sanitizeHeaderKey($name))) {
if('' === ($name_sanitized = $this->sanitizeName($name))) {
return null;
}

foreach($this->headers as $header) {
if($header['header'] === $name) {
return $header['value'];
}
if(!isset($this->headers[ $name_sanitized ])) {
return null;
}

return null;
// return first header value
return $this->headers[ $name_sanitized ][0];
}

/**
* Check if a header exists
* @param string $name The header name
* @return bool
*/
public function has(string $name): bool {
// sanitize
return isset($this->headers[ $this->sanitizeName($name) ]);
}

/**
Expand All @@ -99,46 +131,69 @@ public function get(string $name): ?string {
*/
public function remove(string $name): self {
// sanitize
if('' === ($name = $this->sanitizeHeaderKey($name))) {
if('' === ($name_sanitized = $this->sanitizeName($name))) {
return $this;
}

foreach($this->headers as $key => $header) {
if($header['header'] === $name) {
unset($this->headers[ $key ]);
}
}
unset($this->headers[ $name_sanitized ]);
unset($this->response_codes[ $name_sanitized ]);

return $this;
}

/**
* Check if a header exists
* @param string $name The header name
* @return bool
* Clear all headers
* @return self
*/
public function has(string $name): bool {
// sanitize
if('' === ($name = $this->sanitizeHeaderKey($name))) {
return false;
}
public function clear(): self {
$this->headers = [];
$this->response_codes = [];

foreach($this->headers as $header) {
if($header['header'] === $name) {
return true;
return $this;
}

/**
* Send all headers. Does not check if headers have already been sent
* @return void
*/
public function send(): void {
foreach($this->headers as $key => $headers) {
foreach($headers as $i => $value) {
header(
$key .': '. $value,
!$i,
$this->response_codes[ $key ] ?? 0
);
}
}

return false;
}

/**
* Clear all headers
* @return self
* Sanitize a header name (everything before the first colon)
* @param string $name The header name
* @return string The sanitized header name
*/
public function clear(): self {
$this->headers = [];
public function sanitizeName(string $name): string {
return trim(strtr(
$name,
self::UPPER,
self::LOWER
));
}

/**
* Get the headers as a string
* @return string
*/
public function __toString(): string {
$headers = '';

return $this;
foreach($this->headers as $key => $headers) {
foreach($headers as $i => $header) {
$headers .= $key .': '. $header ."\r\n";
}
}

return $headers;
}
}
49 changes: 25 additions & 24 deletions src/Magnetar/Http/Request.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ class Request {
* @param string $path The requested path (without query string)
*/
public function __construct() {
// set request method
// parse the request's http method
$this->processRequestMethod();

// parse the request path and query string
Expand All @@ -61,16 +61,15 @@ public function __construct() {
// parse the request's cookies
$this->processRequestCookies();

// record request headers
$this->recordHeaders();
// parse the request's headers
$this->processHeaders();
}

/**
* Process the request method
* @return void
*/
protected function processRequestMethod(): void {
// set request method
$this->method = HTTPMethodEnumResolver::resolve(
$_SERVER['REQUEST_METHOD'] ?? null,
HTTPMethodEnum::GET
Expand Down Expand Up @@ -127,30 +126,32 @@ public static function create(): Request {
}

/**
* Record request headers
* @return void
* Get the raw request headers as an associative array.
* Uses filtered values from the $_SERVER global
* @return array
*/
protected function recordHeaders(): void {
$this->headers = new HeaderCollection();

$headers = getallheaders();
protected function rawHeaders(): array {
$headers = [];

if(empty($headers)) {
return;
}

foreach($headers as $key => $value) {
$this->headers->add($key, $value);
foreach($_SERVER as $key => $value) {
if(str_starts_with($key, 'HTTP_')) {
$headers[ strtr(substr($key, 5), '_', '-') ] = $value;
} elseif(in_array($key, ['CONTENT_TYPE', 'CONTENT_LENGTH', 'CONTENT_MD5'])) {
$headers[ strtr($key, '_', '-') ] = $value;
}
}

//// record request headers
//foreach($_SERVER as $key => $value) {
// if(!str_starts_with($key, 'HTTP_')) {
// continue;
// }
//
// $this->headers[ $key ] = $value;
//}
return $headers;
}

/**
* Record request headers
* @return void
*/
protected function processHeaders(): void {
$this->headers = new HeaderCollection(
$this->rawHeaders()
);
}

/**
Expand Down

0 comments on commit 305359f

Please sign in to comment.