UriTrait.php
PHP
Path: src/Http/Message/UriTrait.php
<?php
namespace mini\Http\Message;
use function parse_url;
use Psr\Http\Message\UriInterface;
use JsonSerializable;
use InvalidArgumentException;
/**
* Class simplifies working with URIs
*/
trait UriTrait {
protected string $uri;
/**
* Configure the UriTrait
*
* @param string|Stringable|UriInterface $uri An absolute URL
*/
public function UriTrait(mixed $uri) {
$this->uri = (string) $uri;
}
/**
* Get the scheme of the url
*
* @see Psr\Http\Message\UriInterface::getScheme()
*/
public function getScheme(): string {
return parse_url($this->uri, PHP_URL_SCHEME) ?? '';
}
/**
* Get the hostname of the url
*
* @return mixed
*/
public function getHost(): string
{
return parse_url($this->uri, PHP_URL_HOST) ?? '';
}
/**
* Get the user of the url
*/
public function getUser(): string
{
return parse_url($this->uri, PHP_URL_USER) ?? '';
}
/**
* Get the password of the url
*
* @return string|null
*/
public function getPassword(): ?string
{
return parse_url($this->uri, PHP_URL_PASS);
}
/**
* Parse out the value from the query string and return it.
*
* @param string $param The name of the parameter to remove
* @return mixed
*/
public function getParam($param)
{
$parsed = parse_url($this->uri);
if(empty($parsed['query']))
return NULL;
$parts = static::parseQueryString($parsed['query']);
return $parts[$param] ?? null;
}
/**
* @see Psr\Http\Message\UriInterface::getAuthority()
*/
public function getAuthority(): string {
$parsed = parse_url($this->uri);
$authority = $this->getHost();
if (!$authority) {
return '';
}
if ($userInfo = $this->getUserInfo()) {
$authority = $userInfo."@".$authority;
}
if (null !== ($port = $this->getPort())) {
$authority .= ':'.$port;
}
return $authority;
}
/**
* @see Psr\Http\Message\UriInterface::getUserInfo()
*/
public function getUserInfo(): string {
if ($user = $this->getUser()) {
if ($pass = $this->getPassword()) {
return $user.":".$pass;
}
return $user;
}
return '';
}
/**
* @see Psr\Http\Message\UriInterface::getPort()
*/
public function getPort(): ?int {
return parse_url($this->uri, PHP_URL_PORT);
}
/**
* Returns the path section of the URL up until the query parameter and fragment
*
* @return string
*/
public function getPath(): string {
return parse_url($this->uri, PHP_URL_PATH) ?? '';
}
/**
* Get the entire query part of the url (from the ? until the fragment #)
*
* @see Psr\Http\Message\UriInterface::getQuery()
*
* @return string
*/
public function getQuery(): string
{
return parse_url($this->uri, PHP_URL_QUERY) ?? '';
}
/**
* Get the url fragment.
*
* @see Psr\Http\Message\UriInterface::getFragment()
*
* @return string
*/
public function getFragment(): string
{
return parse_url($this->uri, PHP_URL_FRAGMENT) ?? '';
}
/**
* Get the url fragment.
*
* @see Psr\Http\Message\UriInterface::withScheme()
*
* @return mixed
*/
public function withScheme($scheme): UriInterface {
$c = clone $this;
$c->setScheme($scheme);
return $c;
}
/**
* Get the url fragment.
*
* @see Psr\Http\Message\UriInterface::withUserInfo()
*
* @return mixed
*/
public function withUserInfo($user, $password = null): UriInterface {
$c = clone $this;
$c->setUserInfo($user, $password);
return $c;
}
/**
* Get the url fragment.
*
* @see Psr\Http\Message\UriInterface::withHost()
*
* @return mixed
*/
public function withHost($host): UriInterface {
if (strpos($host, '/') !== false || filter_var("http://".$host, FILTER_VALIDATE_URL) !== 'http://'.$host) {
throw new InvalidArgumentException("Invalid host name");
}
$c = clone $this;
$c->setHost($host);
return $c;
}
/**
* Get the url fragment.
*
* @see Psr\Http\Message\UriInterface::withPort()
*
* @return mixed
*/
public function withPort($port): UriInterface {
$c = clone $this;
$c->setPort($port);
return $c;
}
/**
* Set the path
*
* @see Psr\Http\Message\UriInterface::withPath()
*/
public function withPath($path): UriInterface {
$c = clone $this;
$c->setPath($path);
return $c;
}
/**
* Get the url fragment.
*
* @see Psr\Http\Message\UriInterface::withQuery()
*/
public function withQuery($query): UriInterface {
$c = clone $this;
$c->setQuery($query);
return $c;
}
/**
* Get the url fragment.
*
* @see Psr\Http\Message\UriInterface::withFragment()
*/
public function withFragment($fragment): UriInterface {
$c = clone $this;
$c->setFragment($fragment);
return $c;
}
/**
* When echoing this class, the URL will be displayed.
*
* @see Psr\Http\Message\UriInterface::__toString()
*/
public function __toString()
{
return $this->uri;
}
/**
* @see \JsonSerializable::jsonSerialize()
*/
public function jsonSerialize() {
return $this->__toString();
}
/**
* Set the hostname of the url
*
* @param mixed $value A string or an array to insert as value.
* @return $this
*/
protected function setHost($value)
{
$parsed = parse_url($this->uri);
$parsed['host'] = $value;
$this->uri = static::buildUriString($parsed);
return $this;
}
/**
* Set the scheme of the url (http/https/rtmp etc)
*
* @param string|null $value
*
* @return $this
*/
protected function setScheme(?string $scheme)
{
if (preg_replace('/^[a-z][a-z0-9:.-_]*[a-z0-9]/', '', $scheme) !== '') {
throw new InvalidArgumentException("Invalid scheme '$scheme'");
}
$parsed = parse_url($this->uri);
$parsed['scheme'] = $scheme;
$this->uri = static::buildUriString($parsed);
return $this;
}
/**
* Set the user info of the url
*
* @param string $user The user name to use for authority.
* @param null|string $password The password associated with $user.
*/
protected function setUserInfo(string $user, string $password=null) {
$parsed = parse_url($this->uri);
unset($parsed['user']);
unset($parsed['pass']);
if ($user !== '') {
$parsed['user'] = $user;
if ($password !== null) {
$parsed['pass'] = $password;
}
}
$this->uri = static::buildUriString($parsed);
return $this;
}
/**
* Set the port of the url (http/https/rtmp etc)
*
* @param int|null $port
*
* @return $this
*/
protected function setPort($port)
{
$parsed = parse_url($this->uri);
$parsed['port'] = $port;
$this->uri = static::buildUriString($parsed);
return $this;
}
/**
* Set the path of the url
*
* @param string $value A string to insert as value.
*
* @return $this
*/
protected function setPath(string $path)
{
$parsed = parse_url($this->uri);
if ($path === '' || $path === '/') {
// this is a special case
$parsed['path'] = $path;
} elseif ($path[0] === '/') {
// their path is an absolute path
$parsed['path'] = static::pathShorten($path);
} else {
$parsed['path'] = static::pathShorten(static::pathDirName($parsed['path'] ?? '/').$path);
}
$this->uri = static::buildUriString($parsed);
return $this;
}
/**
* Set the fragment part of the query (after the #)
*
* @param string $value A string
* @return $this
*/
protected function setFragment(string $value) {
$parsed = parse_url($this->uri);
$parsed['fragment'] = $value;
$this->uri = static::buildUriString($parsed);
return $this;
}
/**
* Remove the fragment part of the query (after the #). This removes the entire fragment.
*
* @return $this
*/
protected function unsetFragment()
{
$parsed = parse_url($this->uri);
unset($parsed['fragment']);
$this->uri = static::buildUriString($parsed);
return $this;
}
/**
* Set the entire query (from the ? until the fragment #)
*
* @param string|array $value A string or an array to insert as fragment.
* @return $this
*/
protected function setQuery($value)
{
$parsed = parse_url($this->uri);
$parsed['query'] = $value;
$this->uri = static::buildUriString($parsed);
return $this;
}
/**
* Add or replace a part of the query string
*
* @param string $param The name of the parameter to change
* @param string|array $value A string or an array to insert as value.
* @return $this
*/
protected function setParam($param, $value)
{
$parsed = parse_url($this->uri);
if(empty($parsed['query'])) {
$query = [];
} else {
$query = static::parseQueryString($parsed['query']);
}
$query[$param] = strval($value);
$parsed['query'] = http_build_query($query, '', '&');
$this->uri = static::buildUriString($parsed);
return $this;
}
/**
* Remove a parameter from the query string
*
* @param string $param The name of the parameter to remove
* @return $this
*/
protected function unsetParam($param)
{
$parsed = parse_url($this->uri);
if(empty($parsed['query'])) {
return new static($this->uri);
}
$query = self::parseQueryString($parsed['query']);
unset($query[$param]);
$parsed['query'] = http_build_query($query, '', '&');
$this->uri = static::buildUriString($parsed);
return $this;
}
/**
* Removes all parameters from the query string
*
* @return $this
*/
protected function unsetQuery()
{
$parsed = parse_url($this->uri);
unset($parsed['query']);
$this->uri = static::buildUriString($parsed);
return $this;
}
/**
* Uses the multibyte parse_str version if it exists.
*
* @param string $queryString
* @return array
*/
protected static function parseQueryString(string $queryString): array {
$query = null;
if (function_exists('mb_parse_str')) {
if (!mb_parse_str($queryString, $query)) {
return [];
}
} else {
if (!parse_str($queryString, $query)) {
return [];
}
}
return $query;
}
/**
* Builds a complete URL from a parsed URL, according to parse_url().
*
* Per PSR-7 UriInterface, scheme and host are optional and can be empty strings.
* This method constructs URIs that can be:
* - Full URIs: scheme://host/path
* - Protocol-relative: //host/path
* - Absolute paths: /path
* - Relative paths: path
*
* @param array $parsed Parsed URL components from parse_url()
* @return string The reconstructed URI string
*/
protected static function buildUriString(array $parsed): string {
// Scheme (optional)
$scheme = !empty($parsed['scheme']) ? $parsed['scheme'].':' : '';
// Authority (optional - only built if host is present)
$authority = '';
if (!empty($parsed['host'])) {
$authority = $parsed['host'];
// Add user info if present
if (!empty($parsed['user']) && !empty($parsed['pass'])) {
$authority = $parsed['user'].':'.$parsed['pass'].'@'.$authority;
} elseif (!empty($parsed['user'])) {
$authority = $parsed['user'].'@'.$authority;
}
// Add port if present and non-standard for the scheme
if (!empty($parsed['port'])) {
if (empty($parsed['scheme']) || !isset(Uri::SCHEME_PORTS[$parsed['scheme']]) || Uri::SCHEME_PORTS[$parsed['scheme']] != $parsed['port']) {
$authority .= ':'.$parsed['port'];
}
}
// Authority must be prefixed with //
$authority = '//'.$authority;
}
// Path (optional)
$path = $parsed['path'] ?? '';
if ($path !== '') {
// Per PSR-7: "If the path is rootless and an authority is present,
// the path MUST be prefixed by "/"
if ($authority !== '' && $path[0] !== '/') {
$path = '/'.$path;
}
// Per PSR-7: "If the path is starting with more than one "/" and no
// authority is present, the starting slashes MUST be reduced to one"
if ($authority === '' && str_starts_with($path, '//')) {
$path = '/'.ltrim($path, '/');
}
}
// Query and fragment (optional)
$query = !empty($parsed['query']) ? '?'.$parsed['query'] : '';
$fragment = !empty($parsed['fragment']) ? '#'.$parsed['fragment'] : '';
return $scheme.$authority.$path.$query.$fragment;
}
/**
* Dirname part of the path
*/
protected static function pathDirName(string $path): string {
return preg_replace('/[^\/]+$/', '', $path);
}
/**
* Remove any /./ and resolve any /../ components of a path.
*/
protected static function pathShorten(string $path): string {
if ($path === '') {
return $path;
}
if ($path[0] !== '/') {
throw new \InvalidArgumentException("Invalid path '$path'. Path must be absolute.");
}
// remove leading / and any double slashes in the path
$path = preg_replace('/^\/+|(?<=\/)\/+/', '', $path);
$parts = explode("/", $path);
$result = [];
$length = count($parts);
for ($i = 0; $i < $length; $i++) {
switch ($parts[$i]) {
case '':
if ($i === $length-1) {
$result[] = '';
}
break;
case '.':
if ($i === $length-1) {
$result[] = '';
}
break;
case '..':
array_pop($result);
if ($i === $length-1) {
$result[] = '';
}
break;
default:
$result[] = $parts[$i];
break;
}
}
return '/'.implode("/", $result);
}
}