8000 [HttpFoundation] Add `UrlParser` and `Url` · symfony/symfony@db7c57e · GitHub
[go: up one dir, main page]

Skip to content

Commit db7c57e

Browse files
[HttpFoundation] Add UrlParser and Url
1 parent ccbdc1a commit db7c57e

File tree

18 files changed

+429
-81
lines changed

18 files changed

+429
-81
lines changed

src/Symfony/Component/DependencyInjection/EnvVarProcessor.php

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use Symfony\Component\DependencyInjection\Exception\EnvNotFoundException;
1515
use Symfony\Component\DependencyInjection\Exception\ParameterCircularReferenceException;
1616
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
17+
use Symfony\Component\HttpFoundation\UrlParser\UrlParser;
1718

1819
/**
1920
* @author Nicolas Grekas <p@tchwork.com>
@@ -287,27 +288,20 @@ public function getEnv(string $prefix, string $name, \Closure $getEnv): mixed
287288
}
288289

289290
if ('url' === $prefix) {
290-
$parsedEnv = parse_url($env);
291-
292-
if (false === $parsedEnv) {
291+
try {
292+
$params = UrlParser::parse($env);
293+
} catch (\InvalidArgumentException) {
293294
throw new RuntimeException(sprintf('Invalid URL in env var "%s".', $name));
294295
}
295-
if (!isset($parsedEnv['scheme'], $parsedEnv['host'])) {
296+
297+
if (null === $params->host) {
296298
throw new RuntimeException(sprintf('Invalid URL env var "%s": schema and host expected, "%s" given.', $name, $env));
297299
}
298-
$parsedEnv += [
299-
'port' => null,
300-
'user' => null,
301-
'pass' => null,
302-
'path' => null,
303-
'query' => null,
304-
'fragment' => null,
305-
];
306300

307301
// remove the '/' separator
308-
$parsedEnv['path'] = '/' === ($parsedEnv['path'] ?? '/') ? '' : substr($parsedEnv['path'], 1);
302+
$params->path = '/' === ($params->path ?? '/') ? '' : substr($params->path, 1);
309303

310-
return $parsedEnv;
304+
return (array) $params;
311305
}
312306

313307
if ('query_string' === $prefix) {

src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -624,12 +624,12 @@ public function testDumpedUrlEnvParameters()
624624
$container = new \Symfony_DI_PhpDumper_Test_UrlParameters();
625625
$this->assertSame([
626626
'scheme' => 'postgres',
627+
'user' => 'user',
628+
'pass' => null,
627629
'host' => 'localhost',
628630
'port' => 5432,
629-
'user' => 'user',
630631
'path' => 'database',
631632
'query' => 'sslmode=disable',
632-
'pass' => null,
633633
'fragment' => null,
634634
], $container->getParameter('hello'));
635635
}

src/Symfony/Component/DependencyInjection/Tests/ParameterBag/ParameterBagTest.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -348,8 +348,8 @@ public function testResolveStringWithSpacesReturnsString($expected, $test, $desc
348348
public static function stringsWithSpacesProvider()
349349
{
350350
return [
351-
['bar', '%foo%', 'Parameters must be wrapped by %.'],
352-
['% foo %', '% foo %', 'Parameters should not have spaces.'],
351+
['bar', '%foo%', 'Url must be wrapped by %.'],
352+
['% foo %', '% foo %', 'Url should not have spaces.'],
353353
['{% set my_template = "foo" %}', '{% set my_template = "foo" %}', 'Twig-like strings are not parameters.'],
354354
['50% is less than 100%', '50% is less than 100%', 'Text between % signs is allowed, if there are spaces.'],
355355
];

src/Symfony/Component/HttpFoundation/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ CHANGELOG
55
---
66

77
* Add `UploadedFile::getClientOriginalPath()`
8+
* Add `UrlParser` and `Url`
89

910
7.0
1011
---
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\HttpFoundation\Exception\Parser;
13+
14+
class InvalidUrlException extends \InvalidArgumentException
15+
{
16+
public function __construct()
17+
{
18+
parent::__construct('The URL is invalid.');
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\HttpFoundation\Exception\Parser;
13+
14+
class MissingHostException extends \InvalidArgumentException
15+
{
16+
public function __construct()
17+
{
18+
parent::__construct('The URL must contain a host.');
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\HttpFoundation\Exception\Parser;
13+
14+
class MissingSchemeException extends \InvalidArgumentException
15+
{
16+
public function __construct()
17+
{
18+
parent::__construct('The URL must contain a scheme.');
19+
}
20+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
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\HttpFoundation\Tests\UrlParser;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\HttpFoundation\UrlParser\UrlParser;
16+
use Symfony\Component\HttpFoundation\Exception\Parser\InvalidUrlException;
17+
use Symfony\Component\HttpFoundation\Exception\Parser\MissingSchemeException;
18+
19+
class UrlParserTest extends TestCase
20+
{
21+
public function testInvalidDsn()
22+
{
23+
$this->expectException(InvalidUrlException::class);
24+
$this->expectExceptionMessage('The URL is invalid.');
25+
26+
UrlParser::parse('/search:2019');
27+
}
28+
29+
public function testMissingScheme()
30+
{
31+
$this->expectException(MissingSchemeException::class);
32+
$this->expectExceptionMessage('The URL must contain a scheme.');
33+
34+
UrlParser::parse('://example.com');
35+
}
36+
37+
public function testReturnsFullParsedDsn()
38+
{
39+
$parsedDsn = UrlParser::parse('http://user:pass@localhost:8080/path?query=1#fragment');
40+
41+
$this->assertSame('http', $parsedDsn->scheme);
42+
$this->assertSame('user', $parsedDsn->user);
43+
$this->assertSame('pass', $parsedDsn->pass);
44+
$this->assertSame('localhost', $parsedDsn->host);
45+
$this->assertSame(8080, $parsedDsn->port);
46+
$this->assertSame('/path', $parsedDsn->path);
47+
$this->assertSame('query=1', $parsedDsn->query);
48+
$this->assertSame('fragment', $parsedDsn->fragment);
49+
}
50+
51+
public function testItDecodesByDefault()
52+
{
53+
$parsedDsn = UrlParser::parse('http://user%20one:p%40ss@localhost:8080/path?query=1#fragment');
54+
55+
$this->assertSame('user one', $parsedDsn->user);
56+
$this->assertSame('p@ss', $parsedDsn->pass);
57+
}
58+
59+
public function testDisableDecoding()
60+
{
61+
$parsedDsn = UrlParser::parse('http://user%20one:p%40ss@localhost:8080/path?query=1#fragment', decodeAuth: false);
62+
63+
$this->assertSame('user%20one', $parsedDsn->user);
64+
$this->assertSame('p%40ss', $parsedDsn->pass);
65+
}
66+
67+
public function testEmptyUserAndPasswordAreSetToNull()
68+
{
69+
$parsedDsn = UrlParser::parse('http://@localhost:8080/path?query=1#fragment');
70+
71+
$this->assertNull($parsedDsn->user);
72+
$this->assertNull($parsedDsn->pass);
73+
}
74+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
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\HttpFoundation\Tests\UrlParser;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\HttpFoundation\UrlParser\Url;
16+
17+
class UrlTest extends TestCase
18+
{
19+
/**
20+
* @dataProvider provideUserAndPass
21+
*/
22+
public function testIsAuthenticated(?string $user, ?string $pass, bool $expected)
23+
{
24+
$params = new Url('http', $user, $pass);
25+
26+
$this->assertSame($expected, $params->isAuthenticated());
27+
}
28+
29+
public function provideUserAndPass()
30+
{
31+
yield 'no user, no pass' => [null, null, false];
32+
yield 'user, no pass' => ['user', null, true];
33+
yield 'no user, pass' => [null, 'pass', true];
34+
yield 'user, pass' => ['user', 'pass', true];
35+
}
36+
37+
public function testToString()
38+
{
39+
$params = new Url(
40+
'http',
41+
'user',
42+
'pass',
43+
'localhost',
44+
8080,
45+
'/path',
46+
'query=1',
47+
'fragment'
48+
);
49+
50+
$this->assertSame('http://user:pass@localhost:8080/path?query=1#fragment', (string) $params);
51+
}
52+
53+
public function testToStringReencode()
54+
{
55+
$params = new Url(
56+
'http',
57+
'user one',
58+
'p@ss',
59+
'localhost',
60+
8080,
61+
'/p@th',
62+
'query=1',
63+
'fr%40gment%20with%20spaces'
64+
);
65+
66+
$this->assertSame('http://user%20one:p%40ss@localhost:8080/p@th?query=1#fr%40gment%20with%20spaces', (string) $params);
67+
}
68+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
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\HttpFoundation\UrlParser;
13+
14+
/**
15+
* @author Alexandre Daubois <alex.daubois@gmail.com>
16+
*/
17+
final class Url implements \Stringable
18+
{
19+
public function __construct(
20+
public string $scheme,
21+
public ?string $user = null,
22+
public ?string $pass = null,
23+
public ?string $host = null,
24+
public ?int $port = null,
25+
public ?string $path = null,
26+
public ?string $query = null,
27+
public ?string $fragment = null
28+
) {
29+
}
30+
31+
public function isAuthenticated(): bool
32+
{
33+
return null !== $this->user || null !== $this->pass;
34+
}
35+
36+
public function isScheme(string $scheme): bool
37+
{
38+
return $this->scheme === $scheme;
39+
}
40+
41+
public function __toString(): string
42+
{
43+
$dsn = $this->scheme.'://';
44+
45+
if (null !== $this->user) {
46+
$dsn .= rawurlencode($this->user);
47+
}
48+
49+
if (null !== $this->pass) {
50+
$dsn .= ':'.rawurlencode($this->pass);
51+
}
52+
53+
if (null !== $this->user || null !== $this->pass) {
54+
$dsn .= '@';
55+
}
56+
57+
$dsn .= $this->host;
58+
59+
if (null !== $this->port) {
60+
$dsn .= ':'.$this->port;
61+
}
62+
63+
if (null !== $this->path) {
64+
$dsn .= $this->path;
65+
}
66+
67+
if (null !== $this->query) {
68+
$dsn .= '?'.$this->query;
69+
}
70+
71+
if (null !== $this->fragment) {
72+
$dsn .= '#'.$this->fragment;
73+
}
74+
75+
return $dsn;
76+
}
77+
78+
public function parsedQuery(): array
79+
{
80+
if (null === $this->query) {
81+
return [];
82+
}
83+
84+
parse_str($this->query, $query);
85+
86+
return $query;
87+
}
88+
}

0 commit comments

Comments
 (0)
0