Home grown query/form string encoder.
This commit is contained in:
parent
4f092a3259
commit
346b8a52e2
8 changed files with 277 additions and 55 deletions
2
VERSION
2
VERSION
|
@ -1 +1 @@
|
|||
0.2503.72337
|
||||
0.2503.80055
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// HttpRequest.php
|
||||
// Created: 2022-02-08
|
||||
// Updated: 2025-03-07
|
||||
// Updated: 2025-03-08
|
||||
|
||||
namespace Index\Http;
|
||||
|
||||
|
@ -30,8 +30,8 @@ class HttpRequest extends HttpMessage implements ServerRequestInterface {
|
|||
* @param array<string, mixed> $attributes Attributes derived from the request.
|
||||
* @param string $method HTTP request method.
|
||||
* @param HttpUri $uri HTTP request URI.
|
||||
* @param array<string, string[]> $params HTTP request query parameters.
|
||||
* @param array<string, string> $cookies HTTP request cookies.
|
||||
* @param array<string, list<?string>> $params HTTP request query parameters.
|
||||
* @param array<string, string[]> $cookies HTTP request cookies.
|
||||
* @param array<string, array<object|string>>|object|null $parsedBody Parsed body contents.
|
||||
* @param ?array<string, HttpUploadedFile[]> $uploadedFiles Parsed files.
|
||||
*/
|
||||
|
@ -109,8 +109,8 @@ class HttpRequest extends HttpMessage implements ServerRequestInterface {
|
|||
* @param ?array<string, mixed> $attributes Value you'd otherwise pass to withAttributes (if that existed), null to leave unmodified.
|
||||
* @param ?string $method Value you'd otherwise pass to withMethod, null to leave unmodified.
|
||||
* @param ?UriInterface $uri Value you'd otherwise pass to withUri, null to leave unmodified.
|
||||
* @param ?array<string, string[]> $params Value you'd otherwise pass to withQueryParams, null to leave unmodified.
|
||||
* @param ?array<string, string> $cookies Value you'd otherwise pass to withCookiesParams, null to leave unmodified.
|
||||
* @param ?array<string, list<?string>> $params Value you'd otherwise pass to withQueryParams, null to leave unmodified.
|
||||
* @param ?array<string, string[]> $cookies Value you'd otherwise pass to withCookiesParams, null to leave unmodified.
|
||||
* @param null|array<string, string[]|object[]>|object|false $parsedBody Value you'd otherwise pass to withParsedBody, false to leave unmodified.
|
||||
* @param array<string, HttpUploadedFile[]>|null|false $uploadedFiles Value you'd otherwise pass to withUploadedFiles, false to leave unmodified.
|
||||
* @throws InvalidArgumentException If any of the arguments are not acceptable.
|
||||
|
@ -210,14 +210,14 @@ class HttpRequest extends HttpMessage implements ServerRequestInterface {
|
|||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
* @return array<string, string[]>
|
||||
*/
|
||||
public function getCookieParams(): array {
|
||||
return $this->cookies;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string> $cookies Array of key/value pairs representing cookies.
|
||||
* @param array<string, string[]> $cookies Array of key/value pairs representing cookies.
|
||||
*/
|
||||
public function withCookieParams(array $cookies): HttpRequest {
|
||||
return $this->with(cookies: $cookies);
|
||||
|
@ -236,6 +236,7 @@ class HttpRequest extends HttpMessage implements ServerRequestInterface {
|
|||
/**
|
||||
* Retrieves an HTTP request cookie, or null if it is not present.
|
||||
*
|
||||
* @todo UPDATE FOR THE NEW SYNTAX THINGY WITH THE LIST<STRING>!!!!!!!!!!!
|
||||
* @param string $name Name of the request cookie.
|
||||
* @param int $filter A PHP filter extension filter constant.
|
||||
* @param array<string, mixed>|int $options Options for the PHP filter.
|
||||
|
@ -249,14 +250,14 @@ class HttpRequest extends HttpMessage implements ServerRequestInterface {
|
|||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string[]>
|
||||
* @return array<string, list<?string>>
|
||||
*/
|
||||
public function getQueryParams(): array {
|
||||
return $this->params;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string[]> $query Array of query string arguments, typically from $_GET.
|
||||
* @param array<string, list<?string>> $query Array of query string arguments, typically from $_GET.
|
||||
*/
|
||||
public function withQueryParams(array $query): HttpRequest {
|
||||
return $this->with(params: $query);
|
||||
|
@ -275,38 +276,31 @@ class HttpRequest extends HttpMessage implements ServerRequestInterface {
|
|||
/**
|
||||
* Retrieves all HTTP request query fields as a query string.
|
||||
*
|
||||
* @param bool $spacesAsPlus true if spaces should be represented with a +, false if %20.
|
||||
* @param ?bool $spacesAsPlus null to use the Index url encoder, other wise whether to represent spaces as a + instead of %20 with the legacy encoder.
|
||||
* @return string Query string representation of query fields.
|
||||
*/
|
||||
public function getParamString(bool $spacesAsPlus = false): string {
|
||||
return http_build_query($this->params, '', '&', $spacesAsPlus ? PHP_QUERY_RFC1738 : PHP_QUERY_RFC3986);
|
||||
public function getParamString(?bool $spacesAsPlus = null): string {
|
||||
return $spacesAsPlus === null
|
||||
? HttpUri::buildQueryString($this->params)
|
||||
: http_build_query($this->params, '', '&', $spacesAsPlus ? PHP_QUERY_RFC1738 : PHP_QUERY_RFC3986);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves an HTTP request query field, or null if it is not present.
|
||||
*
|
||||
* @todo UPDATE FOR THE NEW SYNTAX THINGY WITH THE LIST<?STRING>!!!!!!!!!!!
|
||||
* @param string $name Name of the request query field.
|
||||
* @param int $filter A PHP filter extension filter constant.
|
||||
* @param mixed[]|int $options Options for the PHP filter.
|
||||
* @return ?mixed Value of the query field, null if not present.
|
||||
* @param ?scalar $default Default value to fall back on.
|
||||
* @return ?scalar Value of the query field, null if not present.
|
||||
*/
|
||||
public function getParam(string $name, int $filter = FILTER_DEFAULT, array|int $options = 0): mixed {
|
||||
public function getParam(string $name, int $filter = FILTER_DEFAULT, array|int $options = 0, mixed $default = null): mixed {
|
||||
if(!isset($this->params[$name]))
|
||||
return null;
|
||||
return filter_var($this->params[$name], $filter, $options);
|
||||
}
|
||||
return $default;
|
||||
|
||||
/**
|
||||
* Retrieves a form field with filtering and enforcing a scalar value.
|
||||
*
|
||||
* @param string $name Name of the form field.
|
||||
* @param int $filter A PHP filter extension filter constant.
|
||||
* @param array<string, mixed>|int $options Options for the PHP filter.
|
||||
* @return ?scalar Value of the form field, null if not present.
|
||||
*/
|
||||
public function getParamScalar(string $name, int $filter = FILTER_DEFAULT, array|int $options = 0): bool|float|int|string|null {
|
||||
$value = $this->getParam($name, $filter, $options);
|
||||
return is_scalar($value) ? $value : null;
|
||||
$value = filter_var($this->params[$name], $filter, $options);
|
||||
return is_scalar($value) ? $value : $default;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -366,6 +360,33 @@ class HttpRequest extends HttpMessage implements ServerRequestInterface {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a Cookie header string.
|
||||
*
|
||||
* @param string $cookies Cookie header string.
|
||||
* @return array<string, string[]>
|
||||
*/
|
||||
public static function parseCookieString(string $cookies): array {
|
||||
$params = [];
|
||||
|
||||
if($cookies !== '') {
|
||||
$paramParts = explode(';', $cookies);
|
||||
foreach($paramParts as $paramPart) {
|
||||
$parts = explode('=', ltrim($paramPart, ' '), 2);
|
||||
if(count($parts) > 1) {
|
||||
$name = urldecode($parts[0]);
|
||||
$value = urldecode($parts[1]);
|
||||
if(array_key_exists($name, $params))
|
||||
$params[$name][] = $value;
|
||||
else
|
||||
$params[$name] = [$value];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an HttpRequest instance from the current request.
|
||||
*
|
||||
|
@ -396,14 +417,7 @@ class HttpRequest extends HttpMessage implements ServerRequestInterface {
|
|||
$path = '/' . $path;
|
||||
|
||||
$build->path = $path;
|
||||
|
||||
/** @var array<string, string[]> $getVars */
|
||||
$getVars = $_GET;
|
||||
$build->params = $getVars;
|
||||
|
||||
/** @var array<string, string> $cookieVars */
|
||||
$cookieVars = $_COOKIE;
|
||||
$build->cookies = $cookieVars;
|
||||
$build->params = HttpUri::parseQueryString((string)filter_input(INPUT_SERVER, 'QUERY_STRING'));
|
||||
|
||||
$contentType = null;
|
||||
$contentLength = 0;
|
||||
|
@ -418,6 +432,8 @@ class HttpRequest extends HttpMessage implements ServerRequestInterface {
|
|||
}
|
||||
elseif($name === 'content-length')
|
||||
$contentLength = (int)$value;
|
||||
elseif($name === 'cookie')
|
||||
$build->cookies = self::parseCookieString($value);
|
||||
|
||||
$build->setHeader($name, $value);
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// HttpRequestBuilder.php
|
||||
// Created: 2022-02-08
|
||||
// Updated: 2025-03-07
|
||||
// Updated: 2025-03-08
|
||||
|
||||
namespace Index\Http;
|
||||
|
||||
|
@ -44,25 +44,48 @@ final class HttpRequestBuilder extends HttpMessageBuilder {
|
|||
/**
|
||||
* HTTP request query params.
|
||||
*
|
||||
* @var array<string, string[]>
|
||||
* @var array<string, list<?string>>
|
||||
*/
|
||||
public array $params = [];
|
||||
|
||||
/**
|
||||
* HTTP request cookies.
|
||||
*
|
||||
* @var array<string, string>
|
||||
* @var array<string, string[]>
|
||||
*/
|
||||
public array $cookies = [];
|
||||
|
||||
/**
|
||||
* Sets a HTTP request query param value.
|
||||
*
|
||||
* @param string $name Name of the query field.
|
||||
* @param ?string $value Value of the query field.
|
||||
*/
|
||||
public function setParam(string $name, ?string $value): void {
|
||||
$this->params[$name] = [$value];
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a HTTP request query param value.
|
||||
*
|
||||
* @param string $name Name of the query field.
|
||||
* @param ?string $value Value of the query field.
|
||||
*/
|
||||
public function addParam(string $name, ?string $value): void {
|
||||
if(array_key_exists($name, $this->params))
|
||||
$this->params[$name][] = $value;
|
||||
else
|
||||
$this->params[$name] = [$value];
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a HTTP request query param.
|
||||
*
|
||||
* @param string $name Name of the query field.
|
||||
* @param string[] $value Value of the query field.
|
||||
* @param list<?string> $values Value of the query field.
|
||||
*/
|
||||
public function setParam(string $name, array $value): void {
|
||||
$this->params[$name] = $value;
|
||||
public function setParamValues(string $name, array $values): void {
|
||||
$this->params[$name] = $values;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -75,13 +98,36 @@ final class HttpRequestBuilder extends HttpMessageBuilder {
|
|||
}
|
||||
|
||||
/**
|
||||
* Sets a HTTP request cookie.
|
||||
* Sets a value for a HTTP request cookie.
|
||||
*
|
||||
* @param string $name Name of the cookie.
|
||||
* @param string $value Value of the cookie.
|
||||
*/
|
||||
public function setCookie(string $name, string $value): void {
|
||||
$this->cookies[$name] = $value;
|
||||
$this->cookies[$name] = [$value];
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a value for a HTTP request cookie.
|
||||
*
|
||||
* @param string $name Name of the cookie.
|
||||
* @param string $value Value of the cookie.
|
||||
*/
|
||||
public function addCookie(string $name, string $value): void {
|
||||
if(array_key_exists($name, $this->cookies))
|
||||
$this->cookies[$name][] = $value;
|
||||
else
|
||||
$this->cookies[$name] = [$value];
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets values for a HTTP request cookie.
|
||||
*
|
||||
* @param string $name Name of the cookie.
|
||||
* @param string[] $values Values of the cookie.
|
||||
*/
|
||||
public function setCookieValues(string $name, array $values): void {
|
||||
$this->cookies[$name] = $values;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// HttpUri.php
|
||||
// Created: 2025-02-28
|
||||
// Updated: 2025-02-28
|
||||
// Updated: 2025-03-08
|
||||
|
||||
namespace Index\Http;
|
||||
|
||||
|
@ -258,4 +258,58 @@ class HttpUri implements UriInterface, Stringable {
|
|||
|
||||
return $string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a urlencoded query/form string.
|
||||
*
|
||||
* @param string $query urlencoded string.
|
||||
* @return array<string, list<?string>>
|
||||
*/
|
||||
public static function parseQueryString(string $query): array {
|
||||
$params = [];
|
||||
|
||||
if($query !== '') {
|
||||
$paramParts = explode('&', $query);
|
||||
foreach($paramParts as $paramPart) {
|
||||
$parts = explode('=', $paramPart, 2);
|
||||
$name = urldecode($parts[0]);
|
||||
if(!array_key_exists($name, $params))
|
||||
$params[$name] = [];
|
||||
$params[$name][] = count($parts) > 1 ? urldecode($parts[1]) : null;
|
||||
}
|
||||
}
|
||||
|
||||
return $params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a query/form string from params.
|
||||
*
|
||||
* @param array<string, Stringable|scalar|null|list<Stringable|scalar|null>> $params
|
||||
* @return string
|
||||
*/
|
||||
public static function buildQueryString(array $params): string {
|
||||
$parts = [];
|
||||
|
||||
foreach($params as $name => $values) {
|
||||
$base = rawurlencode($name);
|
||||
if($values === null) {
|
||||
$parts[] = $base;
|
||||
continue;
|
||||
}
|
||||
|
||||
if(is_scalar($values) || $values instanceof Stringable)
|
||||
$values = [$values];
|
||||
|
||||
foreach($values as $value) {
|
||||
$part = $base;
|
||||
if(is_scalar($value) || $value instanceof Stringable)
|
||||
$part .= sprintf('=%s', rawurlencode((string)$value));
|
||||
|
||||
$parts[] = $part;
|
||||
}
|
||||
}
|
||||
|
||||
return implode('&', $parts);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
<?php
|
||||
// ArrayUrlRegistry.php
|
||||
// Created: 2024-10-03
|
||||
// Updated: 2024-10-04
|
||||
// Updated: 2025-03-08
|
||||
|
||||
namespace Index\Urls;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use Stringable;
|
||||
use Index\Http\HttpUri;
|
||||
|
||||
/**
|
||||
* Provides an array backed URL registry implementation.
|
||||
|
@ -37,7 +38,7 @@ class ArrayUrlRegistry implements UrlRegistry {
|
|||
];
|
||||
}
|
||||
|
||||
public function format(string $name, array $vars = [], bool $spacesAsPlus = false): string {
|
||||
public function format(string $name, array $vars = [], ?bool $spacesAsPlus = null): string {
|
||||
if(!array_key_exists($name, $this->urls))
|
||||
return '';
|
||||
|
||||
|
@ -53,7 +54,12 @@ class ArrayUrlRegistry implements UrlRegistry {
|
|||
}
|
||||
|
||||
if(!empty($query))
|
||||
$string .= '?' . http_build_query($query, '', '&', $spacesAsPlus ? PHP_QUERY_RFC1738 : PHP_QUERY_RFC3986);
|
||||
$string .= sprintf(
|
||||
'?%s',
|
||||
$spacesAsPlus === null
|
||||
? HttpUri::buildQueryString($query)
|
||||
: http_build_query($query, '', '&', $spacesAsPlus ? PHP_QUERY_RFC1738 : PHP_QUERY_RFC3986)
|
||||
);
|
||||
}
|
||||
|
||||
if(!empty($format['fragment'])) {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// ScopedUrlRegistry.php
|
||||
// Created: 2024-10-03
|
||||
// Updated: 2024-10-04
|
||||
// Updated: 2025-03-08
|
||||
|
||||
namespace Index\Urls;
|
||||
|
||||
|
@ -47,7 +47,7 @@ class ScopedUrlRegistry implements UrlRegistry {
|
|||
);
|
||||
}
|
||||
|
||||
public function format(string $name, array $vars = [], bool $spacesAsPlus = false): string {
|
||||
public function format(string $name, array $vars = [], ?bool $spacesAsPlus = null): string {
|
||||
return $this->registry->format(
|
||||
$this->namePrefix . $name,
|
||||
$vars,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// UrlRegistry.php
|
||||
// Created: 2024-10-03
|
||||
// Updated: 2025-01-18
|
||||
// Updated: 2025-03-08
|
||||
|
||||
namespace Index\Urls;
|
||||
|
||||
|
@ -30,10 +30,10 @@ interface UrlRegistry {
|
|||
*
|
||||
* @param string $name Name of the URL to format.
|
||||
* @param array<string, mixed> $vars Values to replace the variables in the path, query and fragment with.
|
||||
* @param bool $spacesAsPlus Whether to represent spaces as a + instead of %20.
|
||||
* @param ?bool $spacesAsPlus null to use the Index url encoder, other wise whether to represent spaces as a + instead of %20 with the legacy encoder.
|
||||
* @return string Formatted URL.
|
||||
*/
|
||||
public function format(string $name, array $vars = [], bool $spacesAsPlus = false): string;
|
||||
public function format(string $name, array $vars = [], ?bool $spacesAsPlus = null): string;
|
||||
|
||||
/**
|
||||
* Creates a scoped URL registry.
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// HttpUriTest.php
|
||||
// Created: 2025-02-28
|
||||
// Updated: 2025-02-28
|
||||
// Updated: 2025-03-08
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
|
@ -402,4 +402,104 @@ final class HttpUriTest extends TestCase {
|
|||
$expected = 'foo/bar:';
|
||||
$this->assertSame($expected, HttpUri::createUri($expected)->path);
|
||||
}
|
||||
|
||||
/** @return array<array{0: string, 1: array<string, list<?string>>}> */
|
||||
public static function queryStringProvider(): array {
|
||||
return [
|
||||
[
|
||||
'username=meow&password=beans',
|
||||
['username' => ['meow'], 'password' => ['beans']]
|
||||
],
|
||||
[
|
||||
'username=meow&password=beans&password=soap',
|
||||
['username' => ['meow'], 'password' => ['beans', 'soap']]
|
||||
],
|
||||
[
|
||||
'arg&arg&arg=maybe&arg&the=ok',
|
||||
['arg' => [null, null, 'maybe', null], 'the' => ['ok']]
|
||||
],
|
||||
[
|
||||
'array[]=old&array%5B%5D=syntax&array[meow]=soup',
|
||||
['array[]' => ['old', 'syntax'], 'array[meow]' => ['soup']]
|
||||
],
|
||||
[
|
||||
'plus=this+one+uses+plus+as+space&twenty=this%20uses%20percent%20encoding',
|
||||
['plus' => ['this one uses plus as space'], 'twenty' => ['this uses percent encoding']]
|
||||
],
|
||||
[
|
||||
'&&=&&=&', // there's no reason why this shouldn't be valid but it is quirky!
|
||||
['' => [null, null, '', null, '', null]]
|
||||
],
|
||||
[
|
||||
'',
|
||||
[]
|
||||
],
|
||||
[
|
||||
' ',
|
||||
[' ' => [null]]
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/** @param array<string, string[]> $expected */
|
||||
#[DataProvider('queryStringProvider')]
|
||||
public function testParseQueryString(string $queryString, array $expected): void {
|
||||
$this->assertEquals(HttpUri::parseQueryString($queryString), $expected);
|
||||
}
|
||||
|
||||
/** @return array<array{0: array<string, Stringable|scalar|null|list<Stringable|scalar|null>>, 1: string}> */
|
||||
public static function queryParamsProvider(): array {
|
||||
return [
|
||||
[
|
||||
['username' => ['meow'], 'password' => ['beans']],
|
||||
'username=meow&password=beans'
|
||||
],
|
||||
[
|
||||
['username' => ['meow'], 'password' => ['beans', 'soap']],
|
||||
'username=meow&password=beans&password=soap'
|
||||
],
|
||||
[
|
||||
['arg' => [null, null, 'maybe', null], 'the' => ['ok']],
|
||||
'arg&arg&arg=maybe&arg&the=ok'
|
||||
],
|
||||
[
|
||||
['array[]' => ['old', 'syntax'], 'array[meow]' => ['soup']],
|
||||
'array%5B%5D=old&array%5B%5D=syntax&array%5Bmeow%5D=soup'
|
||||
],
|
||||
[
|
||||
['twenty' => ['this one always uses percent'], 'twenty only' => ['this uses percent encoding']],
|
||||
'twenty=this%20one%20always%20uses%20percent&twenty%20only=this%20uses%20percent%20encoding'
|
||||
],
|
||||
[
|
||||
['' => [null, null, '', null, '', null]],
|
||||
'&&=&&=&' // there's no reason why this shouldn't be valid but it is quirky!
|
||||
],
|
||||
[
|
||||
[],
|
||||
''
|
||||
],
|
||||
[
|
||||
[' ' => [null]],
|
||||
'%20'
|
||||
],
|
||||
[ // scalar types
|
||||
['null' => [null], 'int' => [1234], 'float' => [56.78], 'bool' => [true, false], 'string' => ['why not ig']],
|
||||
'null&int=1234&float=56.78&bool=1&bool=&string=why%20not%20ig'
|
||||
],
|
||||
[ // stringable
|
||||
['stringable' => [new class implements Stringable { public function __toString(): string { return 'a'; } }]],
|
||||
'stringable=a'
|
||||
],
|
||||
[ // accept non-array
|
||||
['null' => null, 'int' => 1234, 'float' => 56.78, 'bool' => true, 'string' => 'why not ig'],
|
||||
'null&int=1234&float=56.78&bool=1&string=why%20not%20ig'
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/** @param array<string, list<Stringable|scalar|null>> $queryParams */
|
||||
#[DataProvider('queryParamsProvider')]
|
||||
public function testBuildQueryString(array $queryParams, string $expected): void {
|
||||
$this->assertEquals(HttpUri::buildQueryString($queryParams), $expected);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue