8000 [Uri] Add component · symfony/symfony@85eee43 · GitHub
[go: up one dir, main page]

Skip to content

Commit 85eee43

Browse files
[Uri] Add component
1 parent 9ca558e commit 85eee43

18 files changed

+1167
-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
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
Please do not submit any Pull Requests here. They will be closed.
2+
---
3+
4+
Please submit your PR here instead:
5+
https://github.com/symfony/symfony
6+
7+
This repository is what we call a "subtree split": a read-only subset of that main repository.
8+
We're looking forward to your PR there!
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
name: Check subtree split
2+
3+
on:
4+
pull_request_target:
5+
6+
jobs:
7+
close-pull-request:
8+
runs-on: ubuntu-latest
9+
10+
steps:
11+
- name: Close pull request
12+
uses: actions/github-script@v6
13+
with:
14+
script: |
15+
if (context.repo.owner === "symfony") {
16+
github.rest.issues.createComment({
17+
owner: "symfony",
18+
repo: context.repo.repo,
19+
issue_number: context.issue.number,
20+
body: `
21+
Thanks for your Pull Request! We love contributions.
22+
23+
However, you should instead open your PR on the main repository:
24+
https://github.com/symfony/symfony
25+
26+
This repository is what we call a "subtree split": a read-only subset of that main repository.
27+
We're looking forward to your PR there!
28+
`
29+
});
30+
31+
github.rest.pulls.update({
32+
owner: "symfony",
33+
repo: context.repo.repo,
34+
pull_number: context.issue.number,
35+
state: "closed"
36+
});
37+
}

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+
final 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: 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+
final class UnresolvableUriException extends \RuntimeException
15+
{
16+
public function __construct(string $uri)
17+
{
18+
parent::__construct(sprintf('The URI "%s" cannot be used as a base URI in a resolution.', $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: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
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+
* @experimental
16+
*
17+
* @author Alexandre Daubois <alex.daubois@gmail.com>
18+
*/
19+
final class QueryString implements \Stringable
20+
{
21+
/**
22+
* @var array<string, string|string[]>
23+
*/
24+
private array $parameters = [];
25+
26+
/**
27+
* Parses a URI.
28+
*
29+
* Unlike `parse_str()`, this method does not overwrite duplicate keys but instead
30+
* returns an array of all values for each key:
31+
*
32+
* QueryString::parse('foo=1&foo=2&bar=3'); // stored as ['foo' => ['1', '2'], 'bar' => '3']
33+
*
34+
* `+` are supported in parameter keys and not replaced by an underscore:
35+
*
36+
* QueryString::parse('foo+bar=1'); // stored as ['foo bar' => '1']
37+
*
38+
* `.` and `_` are supported distinct in parameter keys:
39+
*
40+
* QueryString::parse('foo.bar=1'); // stored as ['foo.bar' => '1']
41+
* QueryString::parse('foo_bar=1'); // stored as ['foo_bar' => '1']
42+
*/
43+
public static function parse(string $query): self
44+
{
45+
$parts = explode('&', $query);
46+
$queryString = new self();
47+
48+
foreach ($parts as $part) {
49+
if ('' === $part) {
50+
continue;
51+
}
52+
53+
$part = explode('=', $part, 2);
54+
$key = urldecode($part[0]);
55+
// keys without value will be stored as empty strings, as "parse_str()" does
56+
$value = isset($part[1]) ? urldecode($part[1]) : '';
57+
58+
// take care of nested arrays
59+
if (preg_match_all('/\[(.*?)]/', $key, $matches)) {
60+
$nestedKeys = $matches[1];
61+
// nest the value inside the extracted keys
62+
$value = array_reduce(array_reverse($nestedKeys), static function ($carry, $key) {
63+
return [$key => $carry];
64+
}, $value);
65+
66+
$key = strstr($key, '[', true);
67+
}
68+
69+
if ($queryString->has($key)) {
70+
$queryString->set($key, self::deepMerge((array) $queryString->get($key), (array) $value));
71+
} else {
72+
$queryString->set($key, $value);
73+
}
74+
}
75+
76+
return $queryString;
77+
}
78+
79+
public function has(string $key): bool
80+
{
81+
return \array_key_exists($key, $this->parameters);
82+
}
83+
84+
/**
85+
* Get the first value of the first tuple whose name is `$key`.
86+
*
87+
* @see https://url.spec.whatwg.org/#interface-urlsearchparams
88+
*
89+
* @return string|string[]|null
90+
*/
91+
public function get(string $key): string|array|null
92+
{
93+
$param = $this->parameters[$key] ?? null;
94+
95+
if (\is_array($param) && array_is_list($param)) {
96+
return $param[0];
97+
}
98+
99+
return $param;
100+
}
101+
102+
/**
103+
* Get all values of the tuple whose name is `$key`.
104+
*
105+
* @see https://url.spec.whatwg.org/#interface-urlsearchparams
106+
*
107+
* @return string|string[]|null
108+
*/
109+
public function getAll(string $key): string|array|null
110+
{
111+
return $this->parameters[$key] ?? null;
112+
}
113+
114+
public function set(string $key, array|string|null $value): self
115+
{
116+
$this->parameters[$key] = $value;
117+
118+
return $this;
119+
}
120+
121+
public function remove(string $key): self
122+
{
123+
unset($this->parameters[$key]);
124+
125+
return $this;
126+
}
127+
128+
/**
129+
* @return array<string, string|string[]>
130+
*/
131+
public function all(): array
132+
{
133+
return $this->parameters;
134+
}
135+
136+
public function __toString(): string
137+
{
138+
$parts = [];
139+
foreach (self::flattenParameters($this->parameters) as $key => $values) {
140+
foreach ((array) $values as $value) {
141+
$parts[] = strtr($key, [' ' => '+']).'='.urlencode($value);
142+
}
143+
}
144+
145+
return implode('&', $parts);
146+
}
147+
148+
private static function flattenParameters(array $parameters, string $prefix = ''): array
149+
{
150+
$result = [];
151+
foreach ($parameters as $key => $value) {
152+
$newKey = '' === $prefix ? $key : $prefix.'['.$key.']';
153+
154+
if (\is_array($value)) {
155+
$result += self::flattenParameters($value, $newKey);
156+
} else {
157+
$result[$newKey] = $value;
158+
}
159+
}
160+
161+
return $result;
162+
}
163+
164+
private static function deepMerge(array $parameters, array $newParameters): array
165+
{
166+
foreach ($newParameters as $key => $value) {
167+
if (\is_array($value) && isset($parameters[$key]) && \is_array($parameters[$key])) {
168+
$parameters[$key] = self::deepMerge($parameters[$key], $value);
169+
} elseif (isset($parameters[$key])) {
170+
$merge = array_merge((array) $parameters[$key], (array) $value);
171+
172+
if (is_string($key)) {
173+
$parameters[$key] = $merge;
174+
} else {
175+
$parameters = $merge;
176+
}
177+
} else {
178+
$parameters[$key] = $value;
179+
}
180+
}
181+
182+
return $parameters;
183+
}
184+
}

0 commit comments

Comments
 (0)
0