| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358 |
- <?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;
- use BackedEnum;
- use League\Uri\Contracts\Conditionable;
- use League\Uri\Contracts\FragmentDirective;
- use League\Uri\Contracts\Transformable;
- use League\Uri\Contracts\UriComponentInterface;
- use League\Uri\Exceptions\SyntaxError;
- use SensitiveParameter;
- use Stringable;
- use Throwable;
- use TypeError;
- use Uri\Rfc3986\Uri as Rfc3986Uri;
- use Uri\WhatWg\Url as WhatWgUrl;
- use function is_bool;
- use function str_replace;
- use function strpos;
- final class Builder implements Conditionable, Transformable
- {
- private ?string $scheme = null;
- private ?string $username = null;
- private ?string $password = null;
- private ?string $host = null;
- private ?int $port = null;
- private ?string $path = null;
- private ?string $query = null;
- private ?string $fragment = null;
- public function __construct(
- BackedEnum|Stringable|string|null $scheme = null,
- BackedEnum|Stringable|string|null $username = null,
- #[SensitiveParameter] BackedEnum|Stringable|string|null $password = null,
- BackedEnum|Stringable|string|null $host = null,
- BackedEnum|int|null $port = null,
- BackedEnum|Stringable|string|null $path = null,
- BackedEnum|Stringable|string|null $query = null,
- BackedEnum|Stringable|string|null $fragment = null,
- ) {
- $this
- ->scheme($scheme)
- ->userInfo($username, $password)
- ->host($host)
- ->port($port)
- ->path($path)
- ->query($query)
- ->fragment($fragment);
- }
- /**
- * @throws SyntaxError
- */
- public function scheme(BackedEnum|Stringable|string|null $scheme): self
- {
- $scheme = $this->filterString($scheme);
- if ($scheme !== $this->scheme) {
- UriString::isValidScheme($scheme) || throw new SyntaxError('The scheme `'.$scheme.'` is invalid.');
- $this->scheme = $scheme;
- }
- return $this;
- }
- /**
- * @throws SyntaxError
- */
- public function userInfo(
- BackedEnum|Stringable|string|null $user,
- #[SensitiveParameter] BackedEnum|Stringable|string|null $password = null
- ): static {
- $username = Encoder::encodeUser($this->filterString($user));
- $password = Encoder::encodePassword($this->filterString($password));
- if ($username !== $this->username || $password !== $this->password) {
- $this->username = $username;
- $this->password = $password;
- }
- return $this;
- }
- /**
- * @throws SyntaxError
- */
- public function host(BackedEnum|Stringable|string|null $host): self
- {
- $host = $this->filterString($host);
- if ($host !== $this->host) {
- null === $host
- || HostRecord::isValid($host)
- || throw new SyntaxError('The host `'.$host.'` is invalid.');
- $this->host = $host;
- }
- return $this;
- }
- /**
- * @throws SyntaxError
- * @throws TypeError
- */
- public function port(BackedEnum|int|null $port): self
- {
- if ($port instanceof BackedEnum) {
- 1 === preg_match('/^\d+$/', (string) $port->value)
- || throw new TypeError('The port must be a valid BackedEnum containing a number.');
- $port = (int) $port->value;
- }
- if ($port !== $this->port) {
- null === $port
- || ($port >= 0 && $port < 65535)
- || throw new SyntaxError('The port value must be null or an integer between 0 and 65535.');
- $this->port = $port;
- }
- return $this;
- }
- /**
- * @throws SyntaxError
- */
- public function authority(BackedEnum|Stringable|string|null $authority): self
- {
- ['user' => $user, 'pass' => $pass, 'host' => $host, 'port' => $port] = UriString::parseAuthority($authority);
- return $this
- ->userInfo($user, $pass)
- ->host($host)
- ->port($port);
- }
- /**
- * @throws SyntaxError
- */
- public function path(BackedEnum|Stringable|string|null $path): self
- {
- $path = $this->filterString($path);
- if ($path !== $this->path) {
- $this->path = null !== $path ? Encoder::encodePath($path) : null;
- }
- return $this;
- }
- /**
- * @throws SyntaxError
- */
- public function query(BackedEnum|Stringable|string|null $query): self
- {
- $query = $this->filterString($query);
- if ($query !== $this->query) {
- $this->query = Encoder::encodeQueryOrFragment($query);
- }
- return $this;
- }
- /**
- * @throws SyntaxError
- */
- public function fragment(BackedEnum|Stringable|string|null $fragment): self
- {
- $fragment = $this->filterString($fragment);
- if ($fragment !== $this->fragment) {
- $this->fragment = Encoder::encodeQueryOrFragment($fragment);
- }
- return $this;
- }
- /**
- * Puts back the Builder in a freshly created state.
- */
- public function reset(): self
- {
- $this->scheme = null;
- $this->username = null;
- $this->password = null;
- $this->host = null;
- $this->port = null;
- $this->path = null;
- $this->query = null;
- $this->fragment = null;
- return $this;
- }
- /**
- * Executes the given callback with the current instance
- * and returns the current instance.
- *
- * @param callable(self): self $callback
- */
- public function transform(callable $callback): static
- {
- return $callback($this);
- }
- public function when(callable|bool $condition, callable $onSuccess, ?callable $onFail = null): static
- {
- if (!is_bool($condition)) {
- $condition = $condition($this);
- }
- return match (true) {
- $condition => $onSuccess($this),
- null !== $onFail => $onFail($this),
- default => $this,
- } ?? $this;
- }
- /**
- * @throws SyntaxError if the URI can not be build with the current Builder state
- */
- public function guard(Rfc3986Uri|WhatWgUrl|BackedEnum|Stringable|string|null $baseUri = null): self
- {
- try {
- $this->build($baseUri);
- return $this;
- } catch (Throwable $exception) {
- throw new SyntaxError('The current builder cannot generate a valid URI.', previous: $exception);
- }
- }
- /**
- * Tells whether the URI can be built with the current Builder state.
- */
- public function validate(Rfc3986Uri|WhatWgUrl|BackedEnum|Stringable|string|null $baseUri = null): bool
- {
- try {
- $this->build($baseUri);
- return true;
- } catch (Throwable) {
- return false;
- }
- }
- public function build(Rfc3986Uri|WhatWgUrl|BackedEnum|Stringable|string|null $baseUri = null): Uri
- {
- $authority = $this->buildAuthority();
- $path = $this->buildPath($authority);
- $uriString = UriString::buildUri(
- $this->scheme,
- $authority,
- $path,
- Encoder::encodeQueryOrFragment($this->query),
- Encoder::encodeQueryOrFragment($this->fragment)
- );
- return Uri::new(null === $baseUri ? $uriString : UriString::resolve($uriString, match (true) {
- $baseUri instanceof Rfc3986Uri => $baseUri->toString(),
- $baseUri instanceof WhatWgUrl => $baseUri->toAsciiString(),
- default => $baseUri,
- }));
- }
- /**
- * @throws SyntaxError
- */
- private function buildAuthority(): ?string
- {
- if (null === $this->host) {
- (null === $this->username && null === $this->password && null === $this->port)
- || throw new SyntaxError('The User Information and/or the Port component(s) are set without a Host component being present.');
- return null;
- }
- $authority = $this->host;
- if (null !== $this->username || null !== $this->password) {
- $userInfo = Encoder::encodeUser($this->username);
- if (null !== $this->password) {
- $userInfo .= ':'.Encoder::encodePassword($this->password);
- }
- $authority = $userInfo.'@'.$authority;
- }
- if (null !== $this->port) {
- return $authority.':'.$this->port;
- }
- return $authority;
- }
- /**
- * @throws SyntaxError
- */
- private function buildPath(?string $authority): ?string
- {
- if (null === $this->path || '' === $this->path) {
- return $this->path;
- }
- $path = Encoder::encodePath($this->path);
- if (null !== $authority) {
- return str_starts_with($path, '/') ? $path : '/'.$path;
- }
- if (str_starts_with($path, '//')) {
- return '/.'.$path;
- }
- $colonPos = strpos($path, ':');
- if (false !== $colonPos && null === $this->scheme) {
- $slashPos = strpos($path, '/');
- (false !== $slashPos && $colonPos > $slashPos) || throw new SyntaxError('In absence of the scheme and authority components, the first path segment cannot contain a colon (":") character.');
- }
- return $path;
- }
- /**
- * Filter a string.
- *
- * @throws SyntaxError if the submitted data cannot be converted to string
- */
- private function filterString(BackedEnum|Stringable|string|null $str): ?string
- {
- $str = match (true) {
- $str instanceof FragmentDirective => $str->toFragmentValue(),
- $str instanceof UriComponentInterface => $str->value(),
- $str instanceof BackedEnum => (string) $str->value,
- null === $str => null,
- default => (string) $str,
- };
- if (null === $str) {
- return null;
- }
- $str = str_replace(' ', '%20', $str);
- return UriString::containsRfc3987Chars($str)
- ? $str
- : throw new SyntaxError('The component value `'.$str.'` contains invalid characters.');
- }
- }
|