Skip to content

Commit

Permalink
Improve UriRenderer interface
Browse files Browse the repository at this point in the history
  • Loading branch information
nyamsprod committed Jan 23, 2025
1 parent aaa43e1 commit 13b2852
Show file tree
Hide file tree
Showing 9 changed files with 580 additions and 76 deletions.
6 changes: 3 additions & 3 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,16 @@
"ext-gmp": "*",
"ext-intl": "*",
"ext-mbstring": "*",
"friendsofphp/php-cs-fixer": "^3.67.1",
"friendsofphp/php-cs-fixer": "^3.68.1",
"guzzlehttp/psr7": "^2.7.0",
"http-interop/http-factory-tests": "^2.2",
"laminas/laminas-diactoros": "^3.5.0",
"nyholm/psr7": "^1.8.2",
"phpbench/phpbench": "^1.3.1",
"phpstan/phpstan": "^1.12.15",
"phpstan/phpstan": "^1.12.16",
"phpstan/phpstan-deprecation-rules": "^1.2.1",
"phpstan/phpstan-phpunit": "^1.4.2",
"phpstan/phpstan-strict-rules": "^1.6.1",
"phpstan/phpstan-strict-rules": "^1.6.2",
"phpunit/phpunit": "^10.5.17 || ^11.5.3",
"psr/http-factory": "^1.1.0",
"psr/http-message": "^1.1.0 || ^2.0",
Expand Down
33 changes: 26 additions & 7 deletions docs/uri/7.0/rfc3986.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,26 @@ $uri = Uri::fromRfc8089('file:/etc/fstab');
echo $uri = //returns 'file:///etc/fstab'
~~~

<p class="message-notice"><code>fromRfc8089</code> is added since version <code>7.4.0</code></p>
<p class="message-info"><code>fromRfc8089</code> is added since version <code>7.4.0</code></p>

It is also possible to instantiate a new instance from the following HTTP related object or string>

~~~php
$uri = Uri::fromMarkdownAnchor('[overview](https://uri.thephpleague.com/uri/7.0/)');
echo $uri; //returns 'https://uri.thephpleague.com/uri/7.0/'

$uri = Uri::fromHeaderLinkValue('<https://example.org/>; rel="start"');
echo $uri = //returns 'https://example.org/'

$uri = Uri::fromHtmlAnchor('<a href="/domain-parser/1.0/">uri-hostname-parser</a>');
echo $uri; //returns '/domain-parser/1.0/'

$uri = Uri::fromHtmlLink('<link rel="icon" href="/assets/img/uri-logo.svg" type="image/svg+xml">');
echo $uri = //returns '/assets/img/uri-logo.svg'
~~~

<p class="message-info">The named constructor are available since version <code>7.6.0</code></p>
<p class="message-notice">To use the named constructor in relation to HTML tag, the <code>ext-dom</code> extension must be present.</p>

## URI string representation

Expand Down Expand Up @@ -147,23 +166,23 @@ HTML specific representation are added to allow adding URI to your HTML/Markdown

```php
$uri = Uri::new('eXAMPLE://a/./b/../b/%63/%7bfoo%7d?foo[]=bar');
echo $uri->toMarkdown();
echo $uri->toMarkdownAnchor();
//display '[example://a/b/c/{foo}?foo[]=bar](example://a/./b/../b/%63/%7bfoo%7d?foo%5B%5D=bar)
echo $uri->toMarkdown('my link');
echo $uri->toMarkdownAnchor('my link');
//display '[my link](example://a/./b/../b/%63/%7bfoo%7d?foo%5B%5D=bar)
echo $uri->toAnchorTag();
echo $uri->toHtmlAnchor();
// display '<a href="example://a/./b/../b/%63/%7bfoo%7d?foo%5B%5D=bar">example://a/b/c/{foo}?foo[]=bar</a>'
echo $uri->toAnchorTag('my link');
echo $uri->toHtmlAnchor('my link');
// display '<a href="example://a/./b/../b/%63/%7bfoo%7d?foo%5B%5D=bar">my link</a>'
```

You can also generate the Link `tag` and/or `header` depending on how you want your URI link to be rendered:

```php
$uri = Uri::new('https://example.com/my/css/v1.3');
echo $uri->toLinkTag(['rel' => 'stylesheet']);
echo $uri->toHtmlLink(['rel' => 'stylesheet']);
//display '<link href="https://example.com/my/css/v1.3" rel="stylesheet">
echo $uri->toLinkHeaderValue(['rel' => 'stylesheet']);
echo $uri->toHeaderLinkValue(['rel' => 'stylesheet']);
//display 'https://example.com/my/css/v1.3 ;rel=stylesheet'
```

Expand Down
1 change: 1 addition & 0 deletions interfaces/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ All Notable changes to `League\Uri\Interfaces` will be documented in this file
- `UriString::removeDotSegments`
- `UriString::normalize`
- `UriString::normalizeAuthority`
- `FeatureDetection::supportsDom`

### Fixed

Expand Down
12 changes: 6 additions & 6 deletions interfaces/Contracts/UriRenderer.php
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ public function jsonSerialize(): string;
/**
* Returns the markdown string representation of the anchor tag with the current instance as its href attribute.
*/
public function toMarkdown(?string $linkTextTemplate = null): string;
public function toMarkdownAnchor(?string $linkTextTemplate = null): string;

/**
* Returns the HTML string representation of the anchor tag with the current instance as its href attribute.
Expand All @@ -67,25 +67,25 @@ public function toMarkdown(?string $linkTextTemplate = null): string;
*
* @throws DOMException
*/
public function toAnchorTag(?string $linkTextTemplate = null, iterable $attributes = []): string;
public function toHtmlAnchor(?string $linkTextTemplate = null, iterable $attributes = []): string;

/**
* Returns the Link tag content for the current instance.
*
* @param iterable<string, string|null> $attributes an ordered map of key value. you must quote the value if needed
* @param iterable<string, string|null> $attributes an ordered map of key value
*
* @throws DOMException
*/
public function toLinkTag(iterable $attributes = []): string;
public function toHtmlLink(iterable $attributes = []): string;

/**
* Returns the Link header content for a single item.
*
* @param iterable<string, string|int|float|bool> $parameters an ordered map of key value. you must quote the value if needed
* @param iterable<string, string|int|float|bool|null> $parameters an ordered map of key value.
*
* @see https://www.rfc-editor.org/rfc/rfc7230.html#section-3.2.6
*/
public function toLinkHeaderValue(iterable $parameters = []): string;
public function toHeaderLinkValue(iterable $parameters = []): string;

/**
* Returns the Unix filesystem path. The method returns null for any other scheme except the file scheme.
Expand Down
12 changes: 12 additions & 0 deletions interfaces/FeatureDetection.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
use League\Uri\Exceptions\MissingFeature;
use League\Uri\IPv4\Calculator;

use function extension_loaded;

use const PHP_INT_SIZE;

/**
Expand Down Expand Up @@ -53,4 +55,14 @@ public static function supportsIPv4Conversion(): void
throw new MissingFeature('A '.Calculator::class.' implementation could not be automatically loaded. To perform IPv4 conversion use a x.64 PHP build or install one of the following extension GMP or BCMath. You can also ship your own implmentation.');
}
}

public static function supportsDom(): void
{
static $isSupported = null;
$isSupported = $isSupported ?? extension_loaded('dom');

if (!$isSupported) {
throw new MissingFeature('To use a DOM related feature, the DOM extension must be installed in your system.');
}
}
}
4 changes: 4 additions & 0 deletions uri/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ All Notable changes to `League\Uri` will be documented in this file
- `Uri` implements the new `League\Uri\Contract\UriInspector` interface
- `Uri` implements the new `League\Uri\Contract\UriRenderer` interface
- `Uri::getUser` returns the encoded user component of the URI an alias for `Uri::getUsername`
- `Uri::fromMarkdownAnchor`
- `Uri::fromHtmlAnchor`
- `Uri::fromHtmlLink`
- `Uri::fromHeaderLinkValue`

### Fixed

Expand Down
161 changes: 161 additions & 0 deletions uri/FactoryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -569,4 +569,165 @@ public static function invalidUriWithWhitespaceProvider(): iterable
yield 'uri surrounded by whitespaces' => ['uri' => ' https://a/b?c '];
yield 'uri containing whitespaces' => ['uri' => 'https://a/b ?c'];
}

#[Test]
#[DataProvider('provideAnchorTagHtml')]
public function it_parses_uri_string_from_an_anchor_tag(string $html, ?string $baseUri, string $expected): void
{
self::assertSame($expected, Uri::fromHtmlAnchor($html, $baseUri)->toString());
}

public static function provideAnchorTagHtml(): iterable
{
yield 'empty string' => [
'html' => '<a href=""></a>',
'baseUri' => null,
'expected' => '',
];

yield 'empty string with base URI' => [
'html' => '<a href=""></a>',
'baseUri' => 'https://example.com/',
'expected' => 'https://example.com/',
];

yield 'anchor tag with no base URI' => [
'html' => '<a href="/">foobar</a>',
'baseUri' => null,
'expected' => '/',
];

yield 'multiple anchor tag' => [
'html' => '<a href="/foobar">foobar</a> <a href="https://example.com/yolo">foobar</a>',
'baseUri' => 'https://example.com/',
'expected' => 'https://example.com/foobar',
];
}

#[Test]
#[DataProvider('provideAnchorTagMarkdown')]
public function it_parses_uri_string_from_an_anchor_markdown(string $html, ?string $baseUri, string $expected): void
{
self::assertSame($expected, Uri::fromMarkdownAnchor($html, $baseUri)->toString());
}

public static function provideAnchorTagMarkdown(): iterable
{
yield 'empty string' => [
'html' => '[yolo]()',
'baseUri' => null,
'expected' => '',
];

yield 'empty string with base URI' => [
'html' => '[]()',
'baseUri' => 'https://example.com/',
'expected' => 'https://example.com/',
];

yield 'anchor tag with no base URI' => [
'html' => '[yolo](/)',
'baseUri' => null,
'expected' => '/',
];

yield 'multiple anchor tag' => [
'html' => '[foobar](/foobar) and then later on [foobar](https://example.com/yolo)',
'baseUri' => 'https://example.com/',
'expected' => 'https://example.com/foobar',
];
}

#[Test]
#[DataProvider('provideInvalidMarkdown')]
public function it_fails_to_parse_an_invalid_markdown(string $html): void
{
$this->expectException(InvalidArgumentException::class);
Uri::fromMarkdownAnchor($html);
}

public static function provideInvalidMarkdown(): iterable
{
yield 'missing markdown placeholder' => ['html' => 'this is **markdown**'];
yield 'invalid markdown placeholder; missing URI part' => ['html' => '[this is an imcomplete] http://example.com markdown'];
yield 'invalid markdown placeholder; missing content part' => ['html' => 'this is an imcomplete(http://example.com) markdown'];
}

#[Test]
#[DataProvider('provideHeaderLinkValue')]
public function it_parses_uri_string_from_an_link_header_value(string $html, ?string $baseUri, string $expected): void
{
self::assertSame($expected, Uri::fromHeaderLinkValue($html, $baseUri)->toString());
}

public static function provideHeaderLinkValue(): iterable
{
yield 'empty string' => [
'html' => '<>; rel="previous"',
'baseUri' => null,
'expected' => '',
];

yield 'empty string with base URI' => [
'html' => '<>; rel="next"',
'baseUri' => 'https://example.com/',
'expected' => 'https://example.com/',
];

yield 'URI with base URI' => [
'html' => '</style.css>; rel="stylesheet"',
'baseUri' => 'https://www.example.com',
'expected' => 'https://www.example.com/style.css',
];

yield 'multiple anchor tag' => [
'html' => '</style.css>; rel="stylesheet", </foobar.css>; rel="stylesheet"',
'baseUri' => 'https://example.com/',
'expected' => 'https://example.com/style.css',
];
}

#[Test]
#[DataProvider('provideInvalidHeaderLinkValue')]
public function it_fails_to_parse_an_invalid_http_header_link_with_invalid_characters(string $html): void
{
$this->expectException(InvalidArgumentException::class);

Uri::fromHeaderLinkValue($html);
}

public static function provideInvalidHeaderLinkValue(): iterable
{
yield 'header value with invalid characters' => ['html' => '</style.css>; title="stylesheet"'."\r"];
yield 'header value with missing URI part' => ['html' => '; rel="stylesheet"'];
yield 'header value with missing semicolon' => ['html' => '</style.css> title="stylesheet"'];
yield 'header value with missing parameters' => ['html' => '</style.css>'];
yield 'header value with missing rel parameter' => ['html' => '</style.css> title="stylesheet"'];
}

#[Test]
#[DataProvider('provideInvalidUri')]
public function it_fails_to_parse_with_new(Stringable|string|null $uri): void
{
self::assertNull(Uri::tryNew($uri));
}

public static function provideInvalidUri(): iterable
{
yield 'null value' => ['uri' => null];
yield 'invalid URI' => ['uri' => 'http://example.com/ '];
}

#[Test]
#[DataProvider('provideInvalidUriForResolution')]
public function it_fails_to_parse_with_parse(Stringable|string $uri, Stringable|string|null $baseUri): void
{
self::assertNull(Uri::parse($uri, $baseUri));
}

public static function provideInvalidUriForResolution(): iterable
{
yield 'invalid URI' => ['uri' => ':', 'baseUri' => null];
yield 'invalid resolution with a non absolute URI' => ['uri' => '', 'baseUri' => '/absolute/path'];
}
}
Loading

0 comments on commit 13b2852

Please sign in to comment.