Pandabot/vendor/league/uri-interfaces/KeyValuePair/Converter.php

212 lines
6.3 KiB
PHP
Executable file

<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri\KeyValuePair;
use League\Uri\Contracts\UriComponentInterface;
use League\Uri\Exceptions\SyntaxError;
use Stringable;
use function array_combine;
use function explode;
use function implode;
use function is_float;
use function is_int;
use function is_string;
use function json_encode;
use function preg_match;
use function str_replace;
use const JSON_PRESERVE_ZERO_FRACTION;
use const PHP_QUERY_RFC1738;
use const PHP_QUERY_RFC3986;
final class Converter
{
private const REGEXP_INVALID_CHARS = '/[\x00-\x1f\x7f]/';
/** @var non-empty-string */
private readonly string $separator;
/**
* @param array<string> $fromRfc3986 contains all the RFC3986 encoded characters to be converted
* @param array<string> $toEncoding contains all the expected encoded characters
*/
private function __construct(
string $separator,
private readonly array $fromRfc3986 = [],
private readonly array $toEncoding = [],
) {
if ('' === $separator) {
throw new SyntaxError('The separator character must be a non empty string.');
}
$this->separator = $separator;
}
/**
* @param non-empty-string $separator
*/
public static function new(string $separator): self
{
return new self($separator);
}
/**
* @param non-empty-string $separator
*/
public static function fromRFC3986(string $separator = '&'): self
{
return self::new($separator);
}
/**
* @param non-empty-string $separator
*/
public static function fromRFC1738(string $separator = '&'): self
{
return self::new($separator)
->withEncodingMap(['%20' => '+']);
}
/**
* @param non-empty-string $separator
*
* @see https://url.spec.whatwg.org/#application/x-www-form-urlencoded
*/
public static function fromFormData(string $separator = '&'): self
{
return self::new($separator)
->withEncodingMap(['%20' => '+', '%2A' => '*']);
}
public static function fromEncodingType(int $encType): self
{
return match ($encType) {
PHP_QUERY_RFC3986 => self::fromRFC3986(),
PHP_QUERY_RFC1738 => self::fromRFC1738(),
default => throw new SyntaxError('Unknown or Unsupported encoding.'),
};
}
/**
* @return non-empty-string
*/
public function separator(): string
{
return $this->separator;
}
/**
* @return array<string, string>
*/
public function encodingMap(): array
{
return array_combine($this->fromRfc3986, $this->toEncoding);
}
/**
* @return array<non-empty-list<string|null>>
*/
public function toPairs(Stringable|string|int|float|bool|null $value): array
{
$value = match (true) {
$value instanceof UriComponentInterface => $value->value(),
$value instanceof Stringable, is_int($value) => (string) $value,
false === $value => '0',
true === $value => '1',
default => $value,
};
if (null === $value) {
return [];
}
$value = match (1) {
preg_match(self::REGEXP_INVALID_CHARS, (string) $value) => throw new SyntaxError('Invalid query string: `'.$value.'`.'),
default => str_replace($this->toEncoding, $this->fromRfc3986, (string) $value),
};
return array_map(
fn (string $pair): array => explode('=', $pair, 2) + [1 => null],
explode($this->separator, $value)
);
}
private static function vString(Stringable|string|bool|int|float|null $value): ?string
{
return match (true) {
$value => '1',
false === $value => '0',
null === $value => null,
is_float($value) => (string) json_encode($value, JSON_PRESERVE_ZERO_FRACTION),
default => (string) $value,
};
}
/**
* @param iterable<array{0:string|null, 1:Stringable|string|bool|int|float|null}> $pairs
*/
public function toValue(iterable $pairs): ?string
{
$filteredPairs = [];
foreach ($pairs as $pair) {
$filteredPairs[] = match (true) {
!is_string($pair[0]) => throw new SyntaxError('the pair key MUST be a string;, `'.gettype($pair[0]).'` given.'),
null === $pair[1] => self::vString($pair[0]),
default => self::vString($pair[0]).'='.self::vString($pair[1]),
};
}
return match ([]) {
$filteredPairs => null,
default => str_replace($this->fromRfc3986, $this->toEncoding, implode($this->separator, $filteredPairs)),
};
}
/**
* @param non-empty-string $separator
*/
public function withSeparator(string $separator): self
{
return match ($this->separator) {
$separator => $this,
default => new self($separator, $this->fromRfc3986, $this->toEncoding),
};
}
/**
* Sets the conversion map.
*
* Each key from the iterable structure represents the RFC3986 encoded characters as string,
* while each value represents the expected output encoded characters
*/
public function withEncodingMap(iterable $encodingMap): self
{
$fromRfc3986 = [];
$toEncoding = [];
foreach ($encodingMap as $from => $to) {
[$fromRfc3986[], $toEncoding[]] = match (true) {
!is_string($from) => throw new SyntaxError('The encoding output must be a string; `'.gettype($from).'` given.'),
$to instanceof Stringable,
is_string($to) => [$from, (string) $to],
default => throw new SyntaxError('The encoding output must be a string; `'.gettype($to).'` given.'),
};
}
return match (true) {
$fromRfc3986 !== $this->fromRfc3986,
$toEncoding !== $this->toEncoding => new self($this->separator, $fromRfc3986, $toEncoding),
default => $this,
};
}
}