10000 [Uri] Add component · symfony/symfony@920597a · GitHub
[go: up one dir, main page]

Skip to content

Commit 920597a

Browse files
[Uri] Add component
1 parent 9ca558e commit 920597a

16 files changed

+975
-0
lines changed

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@
112112
"symfony/twig-bundle": "self.version",
113113
"symfony/type-info": "self.version",
114114
"symfony/uid": "self.version",
115+
"symfony/uri": "self.version",
115116
"symfony/validator": "self.version",
116117
"symfony/var-dumper": "self.version",
117118
"symfony/var-exporter": "self.version",
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Tests export-ignore
2+
/phpunit.xml.dist export-ignore
3+
/.git* export-ignore

src/Symfony/Component/Uri/.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
vendor/
2+
composer.lock
3+
phpunit.xml
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
CHANGELOG
2+
=========
3+
4+
7.2
5+
---
6+
7+
* Add the component as experimental
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Uri\Exception;
13+
14+
class InvalidQueryStringException extends \RuntimeException
15+
{
16+
public function __construct(string $queryString)
17+
{
18+
parent::__construct(sprintf('The query string "%s" is invalid.', $queryString));
19+
}
20+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Uri\Exception;
13+
14+
class InvalidUriException extends \RuntimeException
15+
{
16+
public function __construct(string $uri)
17+
{
18+
parent::__construct(sprintf('The URI "%s" is invalid.', $uri));
19+
}
20+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Uri;
13+
14+
/**
15+
* As defined in the Scroll to Text Fragment proposal.
16+
*
17+
* @see https://wicg.github.io/scroll-to-text-fragment/
18+
*
19+
* @experimental
20+
*
21+
* @author Alexandre Daubois <alex.daubois@gmail.com>
22+
*/
23+
final class FragmentTextDirective implements \Stringable
24+
{
25+
public function __construct(
26+
public string $start,
27+
public ?string $end = null,
28+
public ?string $prefix = null,
29+
public ?string $suffix = null,
30+
) {
31+
}
32+
33+
/**
34+
* Dash, comma and ampersand are encoded, @see https://wicg.github.io/scroll-to-text-fragment/#syntax.
35+
*/
36+
public function __toString(): string
37+
{
38+
$encode = static fn (string $value) => strtr($value, ['-' => '%2D', ',' => '%2C', '&' => '%26']);
39+
40+
return ':~:text='
41+
.($this->prefix ? $encode($this->prefix).'-,' : '')
42+
.$encode($this->start)
43+
.($this->end ? ','.$encode($this->end) : '')
44+
.($this->suffix ? ',-'.$encode($this->suffix) : '');
45+
}
46+
}

src/Symfony/Component/Uri/LICENSE

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
Copyright (c) 2024-present Fabien Potencier
2+
3+
Permission is hereby granted, free of charge, to any person obtaining a copy
4+
of this software and associated documentation files (the "Software"), to deal
5+
in the Software without restriction, including without limitation the rights
6+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7+
copies of the Software, and to permit persons to whom the Software is furnished
8+
to do so, subject to the following conditions:
9+
10+
The above copyright notice and this permission notice shall be included in all
11+
copies or substantial portions of the Software.
12+
13+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19+
THE SOFTWARE.
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Uri;
13+
14+
use Symfony\Component\Uri\Exception\InvalidQueryStringException;
15+
16+
/**
17+
* @experimental
18+
*
19+
* @author Alexandre Daubois <alex.daubois@gmail.com>
20+
*/
21+
final class QueryString implements \Stringable
22+
{
23+
/**
24+
* @var array<string, string|string[]>
25+
*/
26+
private array $parameters = [];
27+
28+
/**
29+
* Parses a URI.
30+
*
31+
* Unlike `parse_str()`, this method does not overwrite duplicate keys but instead
32+
* returns an array of all values for each key:
33+
*
34+
* QueryString::parse('foo=1&foo=2&bar=3'); // stored as ['foo' => ['1', '2'], 'bar' => '3']
35+
*
36+
* `+` are supported in parameter keys and not replaced by an underscore:
37+
*
38+
* QueryString::parse('foo+bar=1'); // stored as ['foo bar' => '1']
39+
*
40+
* `.` and `_` are supported distinct in parameter keys:
41+
*
42+
* QueryString::parse('foo.bar=1'); // stored as ['foo.bar' => '1']
43+
* QueryString::parse('foo_bar=1'); // stored as ['foo_bar' => '1']
44+
*/
45+
public static function parse(string $query): QueryString
46+
{
47+
$parts = \explode('&', $query);
48+
$queryString = new self();
49+
50+
foreach ($parts as $part) {
51+
if ('' === $part) {
52+
continue;
53+
}
54+
55+
$part = \explode('=', $part, 2);
56+
$key = \urldecode($part[0]);
57+
// keys without value will be stored as empty strings, as "parse_str()" does
58+
$value = isset($part[1]) ? \urldecode($part[1]) : '';
59+
60+
// take care of nested arrays
61+
if (preg_match_all('/\[(.*?)\]/', $key, $matches)) {
62+
$nestedKeys = $matches[1];
63+
// nest the value inside the extracted keys
64+
$value = array_reduce(array_reverse($nestedKeys), static function ($carry, $key) {
65+
return [$key => $carry];
66+
}, $value);
67+
68+
$key = strstr($key, '[', true);
69+
}
70+
71+
if ($queryString->has($key)) {
72+
$queryString->set($key, \array_merge((array) $queryString->get($key), (array) $value));
73+
} else {
74+
$queryString->set($key, $value);
75+
}
76+
}
77+
78+
return $queryString;
79+
}
80+
81+
public function has(string $key): bool
82+
{
83+
return \array_key_exists($key, $this->parameters);
84+
}
85+
86+
/**
87+
* @return null|string|string[]
88+
*/
89+
public function get(string $key): string|array|null
90+
{
91+
return $this->parameters[$key] ?? null;
92+
}
93+
94+
public function set(string $key, array|string|null $value): self
95+
{
96+
$this->parameters[$key] = $value;
97+
98+
return $this;
99+
}
100+
101+
public function remove(string $key): self
102+
{
103+
unset($this->parameters[$key]);
104+
105+
return $this;
106+
}
107+
108+
/**
109+
* @return array<string, string|string[]>
110+
*/
111+
public function all(): array
112+
{
113+
return $this->parameters;
114+
}
115+
116+
public function __toString(): string
117+
{
118+
$parts = [];
119+
foreach ($this->parameters as $key => $values) {
120+
foreach ((array) $values as $value) {
121+
$parts[] = \urlencode($key).'='.urlencode($value);
122+
}
123+
}
124+
125+
return \implode('&', $parts);
126+
}
127+
}

src/Symfony/Component/Uri/README.md

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
Uri Component
2+
=============
3+
4+
The Uri component is a low-level Symfony components that enhances PHP built-in
5+
features. The primary goal is to have a consistent and object-oriented approach
6+
for `parse_url()` and `parse_str()` functions.
7+
8+
Getting Started
9+
---------------
10+
11+
```bash
12+
composer require symfony/uri
13+
```
14+
15+
Usage
16+
-----
17+
18+
```php
19+
use Symfony\Component\Uri\QueryString;
20+
use Symfony\Component\Uri\Uri;
21+
22+
require 'vendor/autoload.php';
23+
24+
$uri = Uri::parse('https://example.com/foo/bar?baz=qux&arr[key]=foo&arr[another]=bar');
25+
$uri = $uri->withFragmentTextDirective('start', 'end', 'prefix', 'suffix');
26+
27+
echo (string) $uri."\n"; // https://example.com/foo/bar#:~:text=prefix-,start,end,-suffix
28+
29+
$queryString = $uri->query;
30+
$baz = $queryString->get('baz'); // 'qux'
31+
$arr = $queryString->get('arr'); // ['key' => 'foo', 'another' => 'bar']
32+
33+
// Uri decodes the authority part of the URI
34+
$uri = Uri::parse('https://user:p%40ss@host:123/path?query#fragment');
35+
echo $uri->password."\n"; // 'p@ss'
36+
37+
// QueryString makes a difference between '.' and '_'
38+
$queryString = QueryString::parse('foo.bar=1&foo_bar=2');
39+
echo $queryString->get('foo.bar')."\n"; // '1'
40+
echo $queryString->get('foo_bar')."\n"; // '2'
41+
```
42+
43+
Notable Differences With PHP Functions
44+
--------------------------------------
45+
46+
### `parse_url()`
47+
48+
* `parse_url()` **does not** decode the auth component of the URL (user and
49+
pass). This makes it impossible to use the `parse_url()` function to parse
50+
a URL with a username or password that contains a colon (`:`) or
51+
an `@` character.
52+
53+
### `parse_str()`
54+
55+
* `parse_str()` overwrites any duplicate field in the query parameter
56+
(e.g. `?foo=bar&foo=baz` will return `['foo' => 'baz']`). `foo` should be an
57+
array instead with the two values.
58+
* `parse_str()` replaces `.` in the query parameter keys with `_`, thus no
59+
distinction can be done between `foo.bar` and `foo_bar`.
60+
* `parse_str()` doesn't "support" `+` in the parameter keys and replaces them
61+
with `_` instead of a space.
62+
63+
Resources
64+
---------
65+
66+
* [Contributing](https://symfony.com/doc/current/contributing/index.html)
67+
* [Report issues](https://github.com/symfony/symfony/issues) and
68+
[send Pull Requests](https://github.com/symfony/symfony/pulls)
69+
in the [main Symfony repository](https://github.com/symfony/symfony)
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Uri\Tests;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\Uri\FragmentTextDirective;
16+
17+
/**
18+
* @covers \Symfony\Component\Uri\FragmentTextDirective
19+
*/
20+
class FragmentTextDirectiveTest extends TestCase
21+
{
22+
/**
23+
* @dataProvider provideValidFragmentTextDirectives
24+
*/
25+
public function testToString(FragmentTextDirective $fragmentTextDirective, string $expected)
26+
{
27+
$this->assertSame($expected, (string) $fragmentTextDirective);
28+
}
29+
30+
public function testToStringEncodesSpecialCharacters()
31+
{
32+
$fragmentTextDirective = new FragmentTextDirective('st&rt', 'e,nd', 'prefix-', '-&suffix');
33+
34+
$this->assertSame(':~:text=prefix%2D-,st%26rt,e%2Cnd,-%2D%26suffix', (string) $fragmentTextDirective);
35+
}
36+
37+
public static function provideValidFragmentTextDirectives(): iterable
38+
{
39+
yield [new FragmentTextDirective('start'), ':~:text=start'];
40+
yield [new FragmentTextDirective('start', 'end'), ':~:text=start,end'];
41+
yield [new FragmentTextDirective('start', 'end', 'prefix'), ':~:text=prefix-,start,end'];
42+
yield [new FragmentTextDirective('start', 'end', 'prefix', 'suffix'), ':~:text=prefix-,start,end,-suffix'];
43+
yield [new FragmentTextDirective('start', prefix: 'prefix', suffix: 'suffix'), ':~:text=prefix-,start,-suffix'];
44+
yield [new FragmentTextDirective('start', suffix: 'suffix'), ':~:text=start,-suffix'];
45+
yield [new FragmentTextDirective('start', prefix: 'prefix'), ':~:text=prefix-,start'];
46+
}
47+
}

0 commit comments

Comments
 (0)
0