: str_replace(): Passing null to parameter #2 ($replace) of type array|string is deprecated in
use AmpProject\Exception\FailedToParseUrl;
* Helper class to work with URLs.
* @property-read string|null $scheme
* @property-read string|null $host
* @property-read string|null $port
* @property-read string|null $user
* @property-read string|null $pass
* @property-read string|null $path
* @property-read string|null $query
* @property-read string|null $fragment
* @package ampproject/amp-toolbox
const FRAGMENT = 'fragment';
* Regular expression pattern used to collapse the current path ('.').
const COLLAPSE_CURRENT_PATHS_REGEX_PATTERN = '#/\./#';
* Regular expression pattern used to collapse a relative path ('..').
const COLLAPSE_RELATIVE_PATHS_REGEX_PATTERN = '#(?<=/)(?!\.\./)[^/]+/\.\./#';
* Error message to use when the __get() is triggered for an unknown property.
const PROPERTY_GETTER_ERROR_MESSAGE = 'Undefined property: AmpProject\\Url::';
* Default URL parts to use when constructing an absolute URL out of a relative one.
const URL_DEFAULT_PARTS = [
self::HOST => 'example.com',
* @param string|null $url URL.
* @param Url|null $baseUrl Base URL.
* @throws FailedToParseUrl Exception when the URL or Base URL is malformed.
public function __construct($url = null, Url $baseUrl = null)
$parsedUrl = parse_url($url);
if (false === $parsedUrl) {
throw FailedToParseUrl::forUrl($url);
if (isset($parsedUrl[self::PATH]) && 0 !== strpos($parsedUrl[self::PATH], '/')) {
$root = rtrim($baseUrl->path, '/');
$parsedUrl[self::PATH] = $this->unrelativizePath("{$root}/{$parsedUrl[self::PATH]}");
$parsedUrl = array_merge($baseUrl->toArray(), $parsedUrl);
foreach ($parsedUrl as $key => $value) {
if (is_object($this) && property_exists($this, $key)) {
* Eliminate relative segments (../ and ./) from a path.
* @param string $path Path with relative segments. This is not a URL, so no host and no query string.
* @return string Unrelativized path.
private function unrelativizePath($path)
// Eliminate current directory relative paths, like <foo/./bar/./baz.css> => <foo/bar/baz.css>.
self::COLLAPSE_CURRENT_PATHS_REGEX_PATTERN,
// Collapse relative paths, like <foo/bar/../../baz.css> => <baz.css>.
self::COLLAPSE_RELATIVE_PATHS_REGEX_PATTERN,
* Check whether the URL is a valid image source URL.
* @return bool Whether the src string is a valid image source URL.
public function isValidNonDataUrl()
// Bail early on 'data:' assets.
if ($this->scheme === 'data') {
// @TODO: This probably needs additional logic.
$parts = array_merge(self::URL_DEFAULT_PARTS, $this->toArray(true));
return (bool)filter_var(self::toString($parts), FILTER_VALIDATE_URL);
* Get the URL parts as an associative array.
* @param bool $sparse Whether to only include parts with non-empty values.
* @return array Associative array with URL parts.
public function toArray($sparse = false)
self::SCHEME => $this->scheme,
self::HOST => $this->host,
self::PORT => $this->port,
self::USER => $this->user,
self::PASS => $this->pass,
self::PATH => $this->path,
self::QUERY => $this->query,
self::FRAGMENT => $this->fragment,
return $sparse ? array_filter($array) : $array;
public function __toString()
return self::toString($this->toArray());
* Return a provided URL parsed into parts as an assembled string.
* @param array $parts Parts of the URL to assemble.
* @return string Assembled URL string.
public static function toString($parts)
$url = ! empty($parts[self::SCHEME]) ? "{$parts[self::SCHEME]}://" : '//';
if (! empty($parts[self::USER])) {
$url .= "{$parts[self::USER]}";
if (! empty($parts[self::PASS])) {
$url .= ":{$parts[self::PASS]}";
$url .= ! empty($parts[self::HOST]) ? "{$parts[self::HOST]}" : 'localhost';
if (! empty($parts[self::PORT])) {
$url .= ":{$parts[self::PORT]}";
$url .= ! empty($parts[self::PATH]) ? "{$parts[self::PATH]}" : '/';
if (! empty($parts[self::QUERY])) {
$url .= "?{$parts[self::QUERY]}";
if (! empty($parts[self::FRAGMENT])) {
$url .= "#{$parts[self::FRAGMENT]}";
* Magic getter to return the individual parts.
* @param string $name Name of the part to return.
* @return string|null Part string or null if it was not found during parsing.
public function __get($name)
if (array_key_exists($name, self::URL_DEFAULT_PARTS)) {
// Mimic regular PHP behavior for missing notices.
trigger_error(self::PROPERTY_GETTER_ERROR_MESSAGE . $name, E_USER_NOTICE);