Skip to content

Commit

Permalink
Merge pull request #67 from glenncoppens/feature/configure-and-valida…
Browse files Browse the repository at this point in the history
…te-schemes

Add configurable and validatable schemes.
  • Loading branch information
freekmurze authored Jan 5, 2024
2 parents 83a659a + 13edb06 commit e898c99
Show file tree
Hide file tree
Showing 12 changed files with 414 additions and 51 deletions.
82 changes: 58 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,19 @@

A simple package to deal with URLs in your applications.

Retrieve parts of the URL:
## Installation

You can install the package via composer:

```bash
composer require spatie/url
```

## Usage

### Parse and transform a URL

Retrieve any part of the URL:

```php
use Spatie\Url\Url;
Expand All @@ -18,7 +30,10 @@ echo $url->getHost(); // 'spatie.be'
echo $url->getPath(); // '/opensource'
```

Transform any part of the URL (the `Url` class is immutable):
Transform any part of the URL:

> **Note**
> the `Url` class is immutable.
```php
$url = Url::fromString('https://spatie.be/opensource');
Expand All @@ -27,6 +42,37 @@ echo $url->withHost('github.com')->withPath('spatie');
// 'https://github.com/spatie'
```

### Scheme

Transform the URL scheme.
```php
$url = Url::fromString('http://spatie.be/opensource');

echo $url->withScheme('https'); // 'https://spatie.be/opensource'
```

Use a list of allowed schemes.

> **Note**
> each scheme in the list will be sanitized
```php
$url = Url::fromString('https://spatie.be/opensource');

echo $url->withAllowedSchemes(['wss'])->withScheme('wss'); // 'wss://spatie.be/opensource'
```

or pass the list directly to `fromString` as the URL's scheme will be sanitized and validated immediately:

```php
$url = Url::fromString('https://spatie.be/opensource', [...SchemeValidator::VALID_SCHEMES, 'wss']);

echo $url->withScheme('wss'); // 'wss://spatie.be/opensource'
```


### Query parameters

Retrieve and transform query parameters:

```php
Expand All @@ -46,6 +92,8 @@ echo $url->withoutQueryParameter('utm_campaign'); // 'https://spatie.be/opensour
echo $url->withQueryParameters(['utm_campaign' => 'packages']); // 'https://spatie.be/opensource?utm_source=github&utm_campaign=packages'
```

### Path segments

Retrieve path segments:

```php
Expand All @@ -55,6 +103,8 @@ echo $url->getSegment(1); // 'opensource'
echo $url->getSegment(2); // 'laravel'
```

### PSR-7 `UriInterface`

Implements PSR-7's `UriInterface` interface:

```php
Expand All @@ -65,35 +115,19 @@ The [`league/uri`](https://github.com/thephpleague/uri) is a more powerful packa

Spatie is a webdesign agency based in Antwerp, Belgium. You'll find an overview of all our open source projects [on our website](https://spatie.be/opensource).

## Support us

[<img src="https://github-ads.s3.eu-central-1.amazonaws.com/url.jpg?t=1" width="419px" />](https://spatie.be/github-ad-click/url)

We invest a lot of resources into creating [best in class open source packages](https://spatie.be/open-source). You can support us by [buying one of our paid products](https://spatie.be/open-source/support-us).

We highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using. You'll find our address on [our contact page](https://spatie.be/about-us). We publish all received postcards on [our virtual postcard wall](https://spatie.be/open-source/postcards).

## Installation

You can install the package via composer:
## Testing

```bash
composer require spatie/url
composer test
```

## Usage

Usage is pretty straightforward. Check out the code examples at the top of this readme.

## Changelog
## Support us

Please see [CHANGELOG](CHANGELOG.md) for more information what has changed recently.
[<img src="https://github-ads.s3.eu-central-1.amazonaws.com/url.jpg?t=1" width="419px" />](https://spatie.be/github-ad-click/url)

## Testing
We invest a lot of resources into creating [best in class open source packages](https://spatie.be/open-source). You can support us by [buying one of our paid products](https://spatie.be/open-source/support-us).

```bash
composer test
```
We highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using. You'll find our address on [our contact page](https://spatie.be/about-us). We publish all received postcards on [our virtual postcard wall](https://spatie.be/open-source/postcards).

## Changelog

Expand Down
9 changes: 9 additions & 0 deletions src/BaseValidator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

namespace Spatie\Url;

use Spatie\Url\Contracts\Validator;

abstract class BaseValidator implements Validator
{
}
8 changes: 8 additions & 0 deletions src/Contracts/Validator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php

namespace Spatie\Url\Contracts;

interface Validator
{
public function validate(): void;
}
6 changes: 4 additions & 2 deletions src/Exceptions/InvalidArgument.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@

class InvalidArgument extends InvalidArgumentException
{
public static function invalidScheme(string $url): static
public static function invalidScheme(string $scheme, array $allowedSchemes): static
{
return new static("The scheme `{$url}` isn't valid. It should be either `http`, `https`, `mailto` or `tel`.");
$schemes = implode(', ', array_map(fn($scheme) => "`{$scheme}`", $allowedSchemes));

return new static("The scheme `{$scheme}` isn't valid. It should be either {$schemes}.");
}

public static function invalidUrl(string $url): static
Expand Down
65 changes: 65 additions & 0 deletions src/Scheme.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<?php

namespace Spatie\Url;

use Spatie\Macroable\Macroable;
use Stringable;

class Scheme implements Stringable
{
use Macroable;

protected string $scheme;

protected SchemeValidator $validator;

public function __construct(
string $scheme = '',
array|null $allowedSchemes = null,
) {
$this->validator = new SchemeValidator($allowedSchemes);

$this->setScheme($scheme);
}

protected function validate(string $scheme): void
{
$this->validator->setScheme($scheme);

$this->validator->validate();
}

public function getScheme(): string
{
return $this->scheme;
}

public function setScheme(string $scheme): void
{
$sanitizedScheme = $this->validator::sanitizeScheme($scheme);

$this->validate($sanitizedScheme);

$this->scheme = $sanitizedScheme;
}

public function getAllowedSchemes(): array
{
return $this->validator->getAllowedSchemes();
}

public function setAllowedSchemes(array $allowedSchemes): void
{
$this->validator->setAllowedSchemes($allowedSchemes);
}

public function __toString(): string
{
return $this->getScheme();
}

public function __clone()
{
$this->validator = clone $this->validator;
}
}
58 changes: 58 additions & 0 deletions src/SchemeValidator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<?php

namespace Spatie\Url;

use Spatie\Url\Exceptions\InvalidArgument;

class SchemeValidator extends BaseValidator
{
public const VALID_SCHEMES = ['http', 'https', 'mailto', 'tel'];

private string|null $scheme;

public function __construct(
private array|null $allowedSchemes = null
) {
$this->scheme = null;
$this->allowedSchemes = $allowedSchemes ?? self::VALID_SCHEMES;
}

public function validate(): void
{
// '' aka "no scheme" must always be valid
$alwaysAllowedSchemes = [''];

if (! in_array($this->scheme, [...$this->allowedSchemes, ...$alwaysAllowedSchemes])) {
throw InvalidArgument::invalidScheme($this->scheme, $this->allowedSchemes);
}
}

public static function sanitizeScheme(string $scheme): string
{
// TODO: regex to allow correct format according to https://datatracker.ietf.org/doc/html/rfc3986#section-3.1
return strtolower($scheme);
}

public function getScheme(): string|null
{
return $this->scheme;
}

public function setScheme(string $scheme): void
{
$this->scheme = $scheme;
}

public function getAllowedSchemes(): array|null
{
return $this->allowedSchemes;
}

public function setAllowedSchemes(array $allowedSchemes): void
{
$this->allowedSchemes = array_map(
fn($scheme) => static::sanitizeScheme($scheme),
$allowedSchemes
);
}
}
54 changes: 31 additions & 23 deletions src/Url.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ class Url implements UriInterface, Stringable
{
use Macroable;

protected string $scheme = '';
protected Scheme $scheme;

protected string $host = '';

Expand All @@ -27,10 +27,9 @@ class Url implements UriInterface, Stringable

protected string $fragment = '';

public const VALID_SCHEMES = ['http', 'https', 'mailto', 'tel'];

public function __construct()
{
$this->scheme = new Scheme();
$this->query = new QueryParameterBag();
}

Expand All @@ -39,23 +38,33 @@ public static function create(): static
return new static();
}

public static function fromString(string $url): static
public static function fromString(string $url, array|null $allowedSchemes = null): static
{
if (! $parts = parse_url($url)) {
throw InvalidArgument::invalidUrl($url);
$toUrl = new static();

if($allowedSchemes !== null) {
$toUrl = $toUrl->withAllowedSchemes($allowedSchemes);
}

$url = new static();
$url->scheme = isset($parts['scheme']) ? $url->sanitizeScheme($parts['scheme']) : '';
$url->host = $parts['host'] ?? '';
$url->port = $parts['port'] ?? null;
$url->user = $parts['user'] ?? '';
$url->password = $parts['pass'] ?? null;
$url->path = $parts['path'] ?? '/';
$url->query = QueryParameterBag::fromString($parts['query'] ?? '');
$url->fragment = $parts['fragment'] ?? '';
return static::make($url, $toUrl);
}

return $url;
protected static function make(string $fromUrl, self $toUrl): static
{
if (! $parts = parse_url($fromUrl)) {
throw InvalidArgument::invalidUrl($fromUrl);
}

$toUrl->scheme->setScheme(isset($parts['scheme']) ? $parts['scheme'] : '');
$toUrl->host = $parts['host'] ?? '';
$toUrl->port = $parts['port'] ?? null;
$toUrl->user = $parts['user'] ?? '';
$toUrl->password = $parts['pass'] ?? null;
$toUrl->path = $parts['path'] ?? '/';
$toUrl->query = QueryParameterBag::fromString($parts['query'] ?? '');
$toUrl->fragment = $parts['fragment'] ?? '';

return $toUrl;
}

public function getScheme(): string
Expand Down Expand Up @@ -217,20 +226,18 @@ public function withScheme($scheme): static
{
$url = clone $this;

$url->scheme = $this->sanitizeScheme($scheme);
$url->scheme->setScheme($scheme);

return $url;
}

protected function sanitizeScheme(string $scheme): string
public function withAllowedSchemes(array $schemes): static
{
$scheme = strtolower($scheme);
$url = clone $this;

if (! in_array($scheme, static::VALID_SCHEMES)) {
throw InvalidArgument::invalidScheme($scheme);
}
$url->scheme->setAllowedSchemes($schemes);

return $scheme;
return $url;
}

public function withUserInfo($user, $password = null): static
Expand Down Expand Up @@ -361,5 +368,6 @@ public function __toString(): string
public function __clone()
{
$this->query = clone $this->query;
$this->scheme = clone $this->scheme;
}
}
Loading

0 comments on commit e898c99

Please sign in to comment.