Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

[Uri] Add component#57192

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Conversation

@alexandre-daubois
Copy link
Member

@alexandre-dauboisalexandre-daubois commentedMay 27, 2024
edited
Loading

QA
Branch?7.2
Bug fix?no
New feature?yes
Deprecations?no
Issues-
LicenseMIT

The URI component is parsing and building URIs.

This new component is very similar to some other low-level Symfony components that enhances PHP built-in features (e.g. VarExporter, VarDumper, Process to some extent). The primary goal is to have a consistent and object-oriented approach forparse_url() andparse_str() functions.

What issues does it fix?

  • parse_str() overwrites any duplicate field in the query parameter (e.g.?foo=bar&foo=baz will return['foo' => 'baz']).foo should be an array instead with the two values. More info and examples can be found atStackoverflow.
  • parse_str() replaces. in the query parameter keys with_, thus no distinction can be done betweenfoo.bar andfoo_bar.
  • parse_str() doesn't "support"+ in the parameter keys and replaces them with_ instead of a space.
  • parse_url()does not decode the auth component of the URL (user and pass). This makes it impossible to use theparse_url() function to parse a URL with a username or password that contains a colon (:) or an@ character.

Other Considerations

  • Symfony\Component\HttpKernel\UriSigner could be moved to this new component
  • Symfony\Component\HttpFoundation\UrlHelper could be moved to this component
  • Symfony\Component\DomCrawler\UriResolver could be moved to this new component, but it may be replaced by aresolve() method in the new component (as shown below)

Other languages

Specification

TheUri class

The Uri class allows to create and manipulate Uri, as well as parsing strings to build an Uri object.

finalclass Uriimplements \Stringable{publicfunction__construct(publicstring$scheme,public ?string$user =null,public ?string$password =null,public ?string$host =null,public ?int$port =null,public ?string$path =null,public ?QueryString$query =null,public ?string$fragment =null,public ?FragmentTextDirective$fragmentTextDirective =null,    ) {    }/**     * Parses a URL.     *     * The `user` and `pass` keys are url-decoded automatically when parsing.     *     * @throws InvalidUriException     */publicstaticfunctionparse(string$uri):static;/**     * Resolves a relative URI against a base URI.     *     * Uri::resolve('http://example.com', '/foo/bar'); // http://example.com/foo/bar     * Uri::resolve('http://example.com/foo', '/bar'); // http://example.com/bar     * Uri::resolve('http://example.com/foo', 'bar'); // http://example.com/foo/bar     * Uri::resolve('http://example.com/foo/', 'bar'); // http://example.com/foo/bar     * Uri::resolve('http://example.com/foo/', '../bar'); // http://example.com/bar     * Uri::resolve('http://example.com/foo/', '../../bar'); // http://example.com/bar     * Uri::resolve('http://example.com/foo/', '/bar'); // http://example.com/bar     * Uri::resolve('http://example.com/foo/', 'http://example.org/bar'); // http://example.org/bar     */publicstaticfunctionresolve(Uri|string$baseUri,string$relativeUri):string;/**     * Returns a new instance with a new fragment text directive.     */publicfunctionwithFragmentTextDirective(string$start, ?string$end =null, ?string$prefix =null, ?string$suffix =null):static;/**     * Returns a new instance with the host part of the URI converted to ASCII.     *     * @see https://www.unicode.org/reports/tr46/#ToASCII     */publicfunctionwithIdnHostAsAscii():static;/**     * Returns a new instance with the host part of the URI converted to Unicode.     *     * @see https://www.unicode.org/reports/tr46/#ToUnicode     */publicfunctionwithIdnHostAsUnicode():static;publicfunction__toString();}

TheQueryString class

This class manipulates and parses query strings.

finalclass QueryStringimplements \Stringable{/**     * Parses a URI.     *     * Unlike `parse_str()`, this method does not overwrite duplicate keys but instead     * returns an array of all values for each key:     *     * QueryString::parse('foo=1&foo=2&bar=3'); // stored as ['foo' => ['1', '2'], 'bar' => '3']     *     * `+` are supported in parameter keys and not replaced by an underscore:     *     * QueryString::parse('foo+bar=1'); // stored as ['foo bar' => '1']     *     * `.` and `_` are supported distinct in parameter keys:     *     * QueryString::parse('foo.bar=1'); // stored as ['foo.bar' => '1']     * QueryString::parse('foo_bar=1'); // stored as ['foo_bar' => '1']     */publicstaticfunctionparse(string$query):QueryString;publicfunctionhas(string$key):bool;/**     * Get the first value of the first tuple whose name is `$key`.     *     * @see https://url.spec.whatwg.org/#interface-urlsearchparams     *     * @return string|string[]|null     */publicfunctionget(string$key):string|array|null;/**     * Get all values of the tuple whose name is `$key`.     *     * @see https://url.spec.whatwg.org/#interface-urlsearchparams     *     * @return string|string[]|null     */publicfunctiongetAll(string$key):string|array|null;publicfunctionset(string$key,array|string|null$value):self;publicfunctionremove(string$key):self;/**     * @return array<string, string|string[]>     */publicfunctionall():array;publicfunction__toString():string;}

Fragment Text Directives

Fragment Text Directives are supported as well.

finalclass FragmentTextDirectiveimplements \Stringable{publicfunction__construct(publicstring$start,public ?string$end =null,public ?string$prefix =null,public ?string$suffix =null,    ) {    }/**     * Dash, comma and ampersand are encoded, @see https://wicg.github.io/scroll-to-text-fragment/#syntax.     */publicfunction__toString():string;}

Prefix and suffix are respectively suffixed and prefixed by a dash. If the dash is not provided in the string, it
is added automatically:

$uri = Uri::parse('https://tnyholm.se/about');$uri->withFragmentTextDirectives('start','end','prefix','suffix');echo (string)$uri;// https://tnyholm.se/about#:~:text=prefix-,start,end,-suffix

This also works with an existing fragment:

$uri = Uri::parse('https://tnyholm.se/about#existing');$uri->withFragmentTextDirectives('start','end','prefix','suffix');echo (string)$uri;// https://tnyholm.se/about#existing:~:text=prefix-,start,end,-suffix

Dash, ampersand and comma are automatically escaped in the fragment text directives,
as stated in the specification:

$uri = Uri::parse('https://tnyholm.se/about');$uri->withFragmentTextDirectives('sta-r,t','&nd','prefix');echo (string)$uri;// https://tnyholm.se/about#:~:text=prefix-,sta%2Dr%2Ct,%26nd

Usage / leveraging the component

The first place where this component can be leveraged is DSN parsing (initially, the idea came from some issues parsing DSN for the different cache adapters). As a proof of concept, here is an example of how this component can be used, here the Redis cache adapter:

Redis cache adapter using the Uri component
diff --git a/src/Symfony/Component/Cache/Traits/RedisTrait.php b/src/Symfony/Component/Cache/Traits/RedisTrait.phpindex 8183f92935..97ecb07df9 100644--- a/src/Symfony/Component/Cache/Traits/RedisTrait.php+++ b/src/Symfony/Component/Cache/Traits/RedisTrait.php@@ -25,6 +25,8 @@ use Symfony\Component\Cache\Exception\CacheException;use Symfony\Component\Cache\Exception\InvalidArgumentException;use Symfony\Component\Cache\Marshaller\DefaultMarshaller;use Symfony\Component\Cache\Marshaller\MarshallerInterface;+use Symfony\Component\Uri\QueryString;+use Symfony\Component\Uri\Uri;/*** @author Aurimas Niekis <aurimas@niekis.lt>@@ -86,11 +88,8 @@ trait RedisTrait    */   public static function createConnection(#[\SensitiveParameter] string $dsn, array $options = []): \Redis|\RedisArray|\RedisCluster|\Predis\ClientInterface|Relay   {-        if (str_starts_with($dsn, 'redis:')) {-            $scheme = 'redis';-        } elseif (str_starts_with($dsn, 'rediss:')) {-            $scheme = 'rediss';-        } else {+        $dsn = Uri::parse($dsn);+        if (!\in_array($dsn->scheme, ['redis', 'rediss'], true)) {           throw new InvalidArgumentException('Invalid Redis DSN: it does not start with "redis[s]:".');       }@@ -98,68 +97,58 @@ trait RedisTrait           throw new CacheException('Cannot find the "redis" extension nor the "predis/predis" package.');       }-        $params = preg_replace_callback('#^'.$scheme.':(//)?(?:(?:(?<user>[^:@]*+):)?(?<password>[^@]*+)@)?#', function ($m) use (&$auth) {-            if (isset($m['password'])) {-                if (\in_array($m['user'], ['', 'default'], true)) {-                    $auth = rawurldecode($m['password']);-                } else {-                    $auth = [rawurldecode($m['user']), rawurldecode($m['password'])];-                }--                if ('' === $auth) {-                    $auth = null;-                }-            }--            return 'file:'.($m[1] ?? '');-        }, $dsn);+        $hosts = [];+        $query = $dsn->query?->all() ?? [];+        $tls = 'rediss' === $dsn->scheme;+        $tcpScheme = $tls ? 'tls' : 'tcp';-        if (false === $params = parse_url($params)) {-            throw new InvalidArgumentException('Invalid Redis DSN.');+        $auth = null;+        if ($dsn->password) {+            $auth = [$dsn->password];       }-        $query = $hosts = [];--        $tls = 'rediss' === $scheme;-        $tcpScheme = $tls ? 'tls' : 'tcp';--        if (isset($params['query'])) {-            parse_str($params['query'], $query);+        if ($dsn->user) {+            $auth ??= [];+            array_unshift($auth, $dsn->user);+        }-            if (isset($query['host'])) {-                if (!\is_array($hosts = $query['host'])) {+        if (null !== $dsn->query) {+            $queryString = $dsn->query;+            if ($queryString->has('host')) {+                if (!\is_array($hosts = $queryString->get('host'))) {                   throw new InvalidArgumentException('Invalid Redis DSN: query parameter "host" must be an array.');               }+               foreach ($hosts as $host => $parameters) {                   if (\is_string($parameters)) {-                        parse_str($parameters, $parameters);+                        $parameters = QueryString::parse($parameters);                   }                   if (false === $i = strrpos($host, ':')) {-                        $hosts[$host] = ['scheme' => $tcpScheme, 'host' => $host, 'port' => 6379] + $parameters;+                        $hosts[$host] = ['scheme' => $tcpScheme, 'host' => $host, 'port' => 6379] + $parameters->all();                   } elseif ($port = (int) substr($host, 1 + $i)) {-                        $hosts[$host] = ['scheme' => $tcpScheme, 'host' => substr($host, 0, $i), 'port' => $port] + $parameters;+                        $hosts[$host] = ['scheme' => $tcpScheme, 'host' => substr($host, 0, $i), 'port' => $port] + $parameters->all();                   } else {-                        $hosts[$host] = ['scheme' => 'unix', 'path' => substr($host, 0, $i)] + $parameters;+                        $hosts[$host] = ['scheme' => 'unix', 'path' => substr($host, 0, $i)] + $parameters->all();                   }               }               $hosts = array_values($hosts);           }       }-        if (isset($params['host']) || isset($params['path'])) {-            if (!isset($params['dbindex']) && isset($params['path'])) {-                if (preg_match('#/(\d+)?$#', $params['path'], $m)) {-                    $params['dbindex'] = $m[1] ?? '0';-                    $params['path'] = substr($params['path'], 0, -\strlen($m[0]));-                } elseif (isset($params['host'])) {+        if ($dsn->host || $dsn->path) {+            if (!$dsn->query?->has('dbindex') && $dsn->path) {+                if (preg_match('#/(\d+)?$#', $dsn->path, $m)) {+                    $query['dbindex'] = $m[1] ?? '0';+                    $dsn->path = substr($dsn->path, 0, -\strlen($m[0]));+                } elseif ($dsn->host) {                   throw new InvalidArgumentException('Invalid Redis DSN: query parameter "dbindex" must be a number.');               }           }-            if (isset($params['host'])) {-                array_unshift($hosts, ['scheme' => $tcpScheme, 'host' => $params['host'], 'port' => $params['port'] ?? 6379]);+            if ($dsn->host) {+                array_unshift($hosts, ['scheme' => $tcpScheme, 'host' => $dsn->host, 'port' => $dsn->port ?? 6379]);           } else {-                array_unshift($hosts, ['scheme' => 'unix', 'path' => $params['path']]);+                array_unshift($hosts, ['scheme' => 'unix', 'path' => $dsn->path]);           }       }@@ -167,7 +156,7 @@ trait RedisTrait           throw new InvalidArgumentException('Invalid Redis DSN: missing host.');       }-        $params += $query + $options + self::$defaultConnectionOptions;+        $params = $query + $options + self::$defaultConnectionOptions;       if (isset($params['redis_sentinel']) && isset($params['sentinel_master'])) {           throw new InvalidArgumentException('Cannot use both "redis_sentinel" and "sentinel_master" at the same time.');

More generally, every DSN parsing across the Symfony codebase could benefit from this component. I'm thinking about the authority decoding part allowing to put@ in username and passwords in all DSN (which is currently either supported individually, either not supported at all).


Thank you everyone involved in the specs redaction as well as preliminary review. And big thanks to@Nyholm for helping kickstart this new component! 🙏

Nyholm, OskarStark, onEXHovia, kbond, ging-dev, javiereguiluz, yceruto, welcoMattic, alamirault, jdreesen, and 2 more reacted with hooray emojiOskarStark, ging-dev, fsevestre, yceruto, fbourigault, welcoMattic, and javiereguiluz reacted with heart emojidmaicher, darthf1, and sstok reacted with eyes emoji
@ro0NL
Copy link
Contributor

do we ignorePsr\Http\Message\UriInterface deliberately?

@nicolas-grekas
Copy link
Member

Yes, it's not on the radar of comprehensive URI modelling.

alexandre-daubois reacted with thumbs up emoji

@derrabusderrabus added the RFCRFC = Request For Comments (proposals about features that you want to be discussed) labelMay 27, 2024
@derrabus
Copy link
Member

do we ignorePsr\Http\Message\UriInterface deliberately?

We have a bridge for that. ✌️

nicolas-grekas and sstok reacted with thumbs up emoji

@wouterj
Copy link
Member

Hi!

Before going forward with this PR, I feel like there is something we have to discuss first. I think it's good to do an open discussion on the prior art in the PHP community, to be transparent on why a new component would be necessary in the first place.

One library that is my mind immediately is the URI library provided by the PHP league:https://uri.thephpleague.com/ This one has also been brought up in the previous PR proposing a DNS component (#36999), and it looks like the goal and feature set of this proposed new component is very similar to this library.

Has there been any research in whether it is possible to use this library in Symfony instead? Both from a technical perspective as from a maintainer perspective? Have we talked with@nyamsprod about this?
At least from a maintainer perspective, the PHP league's standards already looks very similar to Symfony's and they are a proven organization for building and maintaining robust libraries. Maybe they are willing, with our help, to make the last steps to make Symfony able to use their library? (if there are any steps left?)

Kocal reacted with thumbs up emoji

@nicolas-grekas
Copy link
Member

Thanks for bringing@nyamsprod in the discussion. I met him in Brussels a few months back and told him about this initiative. We could even borrow code, because the code made in the league package is impressive, hopefully with@nyamsprod blessings.

From a "package principles" pov, the League is mostly a bunch of talented individuals that don't work much together, so this doesn't fit for Symfony needs in terms of maintainership. But I'd be super happy to accompany anyone get into the Symfony community!

@alexandre-daubois
Copy link
MemberAuthor

This new component has some pretty strong biases that run counter to what PHP currently offers (typically, support for “_”, “.” and “+” in query strings). It seems complicated to me to bring these biases intothephpleague/uri? In any case, I could understand if the maintainer didn't want to make these changes, which could be some hard breaking changes.

The original idea behind this component is the fact that DSN parsing within Symfony is duplicated code in many places. The first motivation for this component is to be able to support characters encoded in the user and pass, something that is handled individually in the parsing of each place where it's needed in the code. The idea is to factorize all this. Doesn'tthephpleague/uri seem to offer this feature at first glance? I could be wrong, I haven't looked at all the code in detail. 😄

@wouterj
Copy link
Member

wouterj commentedMay 27, 2024
edited
Loading

From a "package principles" pov, the League is mostly a bunch of talented individuals that don't work much together, so this doesn't fit for Symfony needs in terms of maintainership. But I'd be super happy to accompany anyone get into the Symfony community!

Yet, we already use the library as a core dependency of one of our components:

And we have more core dependencies that are maintained by a single maintainer.

Of course, we need to discuss this, but some core team members already maintain packages in the PHP league. So we've even solved a part of "what if this project becomes abandoned", as core team members within PHP league can take it over more easily than if Symfony had to fork the project (of course, if this happens we need to discuss and ask for permissions for this then).

So I think it's unfair to reject the idea solely based on this argument. Let's investigate the technical side as well (and check with the@nyamsprod about his opinion/idea of the matter).

@nyamsprod
Copy link

to@alexandre-daubois,@wouterj and to everyone hi from Brussels! Since I have been mentioned I will quickly clarify some things.

I have not check the code but the premise for this component do align with those that guide me when developing league/uri. The league/uri ships with its own query parser and url parser for the same reason you are developing this package as far as I understood. This is illustrated in the first example on the documentation homepage for the package 😉 .
The goal of the library is to be as interop as possible and to follow as much as possible established RFC and standards. The packages, there are 3 of them, do a lot more but that's not the point of this response to elaborate on that.

In all fairness, I would have hope and prefer that the contribution could be made directly to The league/uri package but as@nicolas-grekas stated maybe the philosophy and the requirements between your organisation and my way of maintaining the package do not 100% aligned. That being said I believe they are parts of Symfony which do use my package already and I have already merged contributions from people from the Symfony in league/uri so to improve the interop between us. I even recall telling to@tgalopin about the incoming release of v7 so that he can prepare his component about the changes I was making. In other words, there are precedent for mutual participation/coordination in improving league/uri and Symfony.

Last but not least, I indeed met@nicolas-grekas in Brussels and he did ask me if I was OK if people were to use my code elsewhere. Back then I thought he was referring to another package of mine (https://github.com/bakame-php/http-structured-fields) as we discuss about it more in depth but to me it is all the same so I re-iterate the fact that I am ok with what suit you the best. I have no desire to impose anything on anybody, the choice is all yours.

I hope this lengthy response does answer or clarify any doubt from anyone. We are here for the better of PHP anyway so let's just do that one way or another!

peace out!

smnandre, ker0x, OskarStark, alexandre-daubois, welcoMattic, and skigun reacted with heart emoji

@nyamsprod
Copy link

On another note you wrote. I just re-read the bullet points:

parse_str() overwrites any duplicate field in the query parameter (e.g. ?foo=bar&foo=baz will return ['foo' => 'baz']). foo should be an array instead with the two values. More info and examples can be found atStackoverflow.

I used this approach in an EOL version of league/uri and it is the wrong approach you should look into theLiving Standard. There you will see that the correct way to represents query string parameters is a collection of KeyValue Pair as those will guarantee that building and parsing query strings will always return the same value in the same order (might be crucial for URL signing if I understood correctly what you want to achieve).

league/uri-components has a PHP implementation of the Living standard you may use as a template or reference.

valtzu reacted with thumbs up emojialexandre-daubois and welcoMattic reacted with heart emoji

@OskarStark
Copy link
Contributor

Refs#57190

alexandre-daubois reacted with thumbs up emoji

@alexandre-daubois
Copy link
MemberAuthor

alexandre-daubois commentedMay 28, 2024
edited
Loading

Thank you very much for your message Ignace and for these very complete explanations! I think this component makes sense within Symfony, the APIs between your packages and this one are quite different. The purpose of this component is to respond to problems we've encountered in the Symfony codebase (the decoding of user and password being the first ones we wanted to solve) while remaining fairly minimal. When we wrote the specs for the component, we thought about what we wanted in it and also what we didn't want, to keep the scope fairly restricted. This is just my opinion, let's see other people's opinions of course! 😃

In any case, thank you very much for raising the query string point and avoiding making a mistake. I've updated to respect the URL Living Standard document. Also thanks for theleague/uri-components link, which helped me understand what it's all about!

@alexandre-dauboisalexandre-dauboisforce-pushed theuri-component branch 2 times, most recently from4e8bd73 to7d195deCompareMay 28, 2024 07:59
@ro0NL
Copy link
Contributor

what about templated URIs? was it considered?

@alexandre-daubois
Copy link
MemberAuthor

@ro0NL yes! We considered it while writing the specs. I did not implement it in this first iteration, but it definitely has its place here in my opinion

@wouterj
Copy link
Member

wouterj commentedMay 28, 2024
edited
Loading

In advance: I'm sorry to still be pushing back here. This is nothing against the work and thoughts that has been put in this PR.

I'm not yet convinced that we need a Uri component at all. Rather than building a slightly smaller component that borrows code and ideas from an existing library, I think we should always favor using the existing library if there are no major compelling reasons to not do so (and given it's already a dependency of Symfony, I guess we've already established in the past there aren't).

As an example, I've written a fully compatible version of theRedisTrait using the league libraries. As you can see, the difference in code with the patch in the PR description is almost negligible.

Redis cache adapter with league/uri library
diff --git a/src/Symfony/Component/Cache/Traits/RedisTrait.php b/src/Symfony/Component/Cache/Traits/RedisTrait.phpindex 8183f92935..9e2163d73e 100644--- a/src/Symfony/Component/Cache/Traits/RedisTrait.php+++ b/src/Symfony/Component/Cache/Traits/RedisTrait.php@@ -11,6 +11,9 @@  namespace Symfony\Component\Cache\Traits;+use League\Uri\Components\Query;+use League\Uri\Components\UserInfo;+use League\Uri\Uri; use Predis\Command\Redis\UNLINK; use Predis\Connection\Aggregate\ClusterInterface; use Predis\Connection\Aggregate\RedisCluster;@@ -86,11 +89,8 @@ trait RedisTrait      */     public static function createConnection(#[\SensitiveParameter] string $dsn, array $options = []): \Redis|\RedisArray|\RedisCluster|\Predis\ClientInterface|Relay     {-        if (str_starts_with($dsn, 'redis:')) {-            $scheme = 'redis';-        } elseif (str_starts_with($dsn, 'rediss:')) {-            $scheme = 'rediss';-        } else {+        $dsn = Uri::new($dsn);+        if (!\in_array($dsn->getScheme(), ['redis', 'rediss'], true)) {             throw new InvalidArgumentException('Invalid Redis DSN: it does not start with "redis[s]:".');         }@@ -98,68 +98,54 @@ trait RedisTrait             throw new CacheException('Cannot find the "redis" extension nor the "predis/predis" package.');         }-        $params = preg_replace_callback('#^'.$scheme.':(//)?(?:(?:(?<user>[^:@]*+):)?(?<password>[^@]*+)@)?#', function ($m) use (&$auth) {-            if (isset($m['password'])) {-                if (\in_array($m['user'], ['', 'default'], true)) {-                    $auth = rawurldecode($m['password']);-                } else {-                    $auth = [rawurldecode($m['user']), rawurldecode($m['password'])];-                }--                if ('' === $auth) {-                    $auth = null;-                }-            }--            return 'file:'.($m[1] ?? '');-        }, $dsn);--        if (false === $params = parse_url($params)) {-            throw new InvalidArgumentException('Invalid Redis DSN.');-        }--        $query = $hosts = [];--        $tls = 'rediss' === $scheme;+        $hosts = [];+        $query = Query::new($dsn->getQuery());+        $tls = 'rediss' === $dsn->getScheme();         $tcpScheme = $tls ? 'tls' : 'tcp';+        $userInfo = UserInfo::new($dsn->getUserInfo());+        $auth = null;+        if ($userInfo->getPass()) {+            $auth = [$userInfo->getPass()];+        }-        if (isset($params['query'])) {-            parse_str($params['query'], $query);+        if ($userInfo->getUser()) {+            $auth ??= [];+            array_unshift($auth, $userInfo->getUser());+        }-            if (isset($query['host'])) {-                if (!\is_array($hosts = $query['host'])) {-                    throw new InvalidArgumentException('Invalid Redis DSN: query parameter "host" must be an array.');+        if ($query->hasParameter('host')) {+            $hosts = $query->parameter('host');+            foreach ($hosts as $host => $parameters) {+                if (\is_string($parameters)) {+                    $parameters = Query::new($parameters);                 }-                foreach ($hosts as $host => $parameters) {-                    if (\is_string($parameters)) {-                        parse_str($parameters, $parameters);-                    }-                    if (false === $i = strrpos($host, ':')) {-                        $hosts[$host] = ['scheme' => $tcpScheme, 'host' => $host, 'port' => 6379] + $parameters;-                    } elseif ($port = (int) substr($host, 1 + $i)) {-                        $hosts[$host] = ['scheme' => $tcpScheme, 'host' => substr($host, 0, $i), 'port' => $port] + $parameters;-                    } else {-                        $hosts[$host] = ['scheme' => 'unix', 'path' => substr($host, 0, $i)] + $parameters;-                    }+                if (false === $i = strrpos($host, ':')) {+                    $hosts[$host] = ['scheme' => $tcpScheme, 'host' => $host, 'port' => 6379] + $parameters->parameters();+                } elseif ($port = (int) substr($host, 1 + $i)) {+                    $hosts[$host] = ['scheme' => $tcpScheme, 'host' => substr($host, 0, $i), 'port' => $port] + $parameters->parameters();+                } else {+                    $hosts[$host] = ['scheme' => 'unix', 'path' => substr($host, 0, $i)] + $parameters->parameters();                 }-                $hosts = array_values($hosts);             }+            $hosts = array_values($hosts);         }-        if (isset($params['host']) || isset($params['path'])) {-            if (!isset($params['dbindex']) && isset($params['path'])) {-                if (preg_match('#/(\d+)?$#', $params['path'], $m)) {-                    $params['dbindex'] = $m[1] ?? '0';-                    $params['path'] = substr($params['path'], 0, -\strlen($m[0]));-                } elseif (isset($params['host'])) {+        $host = $dsn->getHost();+        $path = $dsn->getPath();+        if ($host || $path) {+            if (!$query->hasParameter('dbindex') && $path) {+                if (preg_match('#/(\d+)?$#', $path, $m)) {+                    $query = $query->appendTo('dbindex', $m[1] ?? '0');+                    $path = substr($path, 0, -\strlen($m[0]));+                } elseif ($host) {                     throw new InvalidArgumentException('Invalid Redis DSN: query parameter "dbindex" must be a number.');                 }             }-            if (isset($params['host'])) {-                array_unshift($hosts, ['scheme' => $tcpScheme, 'host' => $params['host'], 'port' => $params['port'] ?? 6379]);+            if ($host) {+                array_unshift($hosts, ['scheme' => $tcpScheme, 'host' => $host, 'port' => $dsn->getPort() ?? 6379]);             } else {-                array_unshift($hosts, ['scheme' => 'unix', 'path' => $params['path']]);+                array_unshift($hosts, ['scheme' => 'unix', 'path' => $path]);             }         }@@ -167,7 +153,7 @@ trait RedisTrait             throw new InvalidArgumentException('Invalid Redis DSN: missing host.');         }-        $params += $query + $options + self::$defaultConnectionOptions;+        $params = $query->parameters() + $options + self::$defaultConnectionOptions;          if (isset($params['redis_sentinel']) && isset($params['sentinel_master'])) {             throw new InvalidArgumentException('Cannot use both "redis_sentinel" and "sentinel_master" at the same time.');

Another good reason to useleague/uri instead imho would be that we give the users aUri class that implements RFC-3986 and the URL Living Standard, rather than aUri class which is scoped specifically for internal usage of Symfony's DSN env vars. The latter can cause for issues when users decide to start using the Uri class for their own non-DSN use-cases.
And having only 29 small to medium sized classes inleague/uri andleague/uri-components together doesn't sound "bloated" enough to warrant a Symfony component with a smaller feature-set imho.

(and thanks@nyamsprod for your lengthy response!)

rylixs, ker0x, dmaicher, kelunik, jvasseur, and pbowyer reacted with thumbs up emoji

@alexandre-daubois
Copy link
MemberAuthor

alexandre-daubois commentedMay 29, 2024
edited
Loading

One thing that bothers me about the example you give for the Redis cache adapter is that it requires the addition of two external dependencies (uri anduri-components) instead of just one internal one. I think it's not ideal to have to add2 external packages to achieve the same result.

Even if this dependency already exists within the framework, do we want to further increase the coupling to this package? Again, to achieve the same result, the two dependencies will be needed in the different components to properly parse DSNs. Indeed,uri is required to parse the URL, anduri-components is required to parse the user info and do the decoding stuff. Seems heavy to me.

The latter can cause for issues when users decide to start using the Uri class for their own non-DSN use-cases.

I guess it is the same problem with every class in the framework, we cannot limit their usage if a user tries to use it for something else. But I may be missing something, did you have a particular example in mind?

@nicolas-grekas
Copy link
Member

Actually, in terms of deps-hell, there is a show stopper: this requires PSR-7. We went this way for HtmlSanitizer because it's a minor component, but that's nontheless still a problem to me. I certainly do not want to have to require PSR-7 and its myriad of other deps when using HttpClient, Cache, HttpFoundation (or even HtmlSanitizer).

welcoMattic reacted with thumbs up emoji

@Nyholm
Copy link
Member

Nyholm commentedMay 29, 2024
edited
Loading

To build on what Nicolas is saying. This PR suggest to introduce 6 new classes. We cannot compare that to the 75 classes added if you docomposer require league/uri-components

See all classes
vendor/league/uri-components/IPv4Normalizer.phpvendor/league/uri-components/UriModifier.phpvendor/league/uri-components/Components/Domain.phpvendor/league/uri-components/Components/Port.phpvendor/league/uri-components/Components/Host.phpvendor/league/uri-components/Components/Authority.phpvendor/league/uri-components/Components/HierarchicalPath.phpvendor/league/uri-components/Components/Path.phpvendor/league/uri-components/Components/UserInfo.phpvendor/league/uri-components/Components/DataPath.phpvendor/league/uri-components/Components/Fragment.phpvendor/league/uri-components/Components/Component.phpvendor/league/uri-components/Components/Query.phpvendor/league/uri-components/Components/URLSearchParams.phpvendor/league/uri-components/Components/Scheme.phpvendor/league/uri-components/Modifier.phpvendor/league/uri-interfaces/FeatureDetection.phpvendor/league/uri-interfaces/Encoder.phpvendor/league/uri-interfaces/Contracts/UriInterface.phpvendor/league/uri-interfaces/Contracts/UriComponentInterface.phpvendor/league/uri-interfaces/Contracts/FragmentInterface.phpvendor/league/uri-interfaces/Contracts/IpHostInterface.phpvendor/league/uri-interfaces/Contracts/UserInfoInterface.phpvendor/league/uri-interfaces/Contracts/AuthorityInterface.phpvendor/league/uri-interfaces/Contracts/HostInterface.phpvendor/league/uri-interfaces/Contracts/PathInterface.phpvendor/league/uri-interfaces/Contracts/DomainHostInterface.phpvendor/league/uri-interfaces/Contracts/PortInterface.phpvendor/league/uri-interfaces/Contracts/UriException.phpvendor/league/uri-interfaces/Contracts/DataPathInterface.phpvendor/league/uri-interfaces/Contracts/QueryInterface.phpvendor/league/uri-interfaces/Contracts/UriAccess.phpvendor/league/uri-interfaces/Contracts/SegmentedPathInterface.phpvendor/league/uri-interfaces/KeyValuePair/Converter.phpvendor/league/uri-interfaces/UriString.phpvendor/league/uri-interfaces/Idna/Option.phpvendor/league/uri-interfaces/Idna/Result.phpvendor/league/uri-interfaces/Idna/Error.phpvendor/league/uri-interfaces/Idna/Converter.phpvendor/league/uri-interfaces/Exceptions/OffsetOutOfBounds.phpvendor/league/uri-interfaces/Exceptions/MissingFeature.phpvendor/league/uri-interfaces/Exceptions/ConversionFailed.phpvendor/league/uri-interfaces/Exceptions/SyntaxError.phpvendor/league/uri-interfaces/IPv4/GMPCalculator.phpvendor/league/uri-interfaces/IPv4/Calculator.phpvendor/league/uri-interfaces/IPv4/Converter.phpvendor/league/uri-interfaces/IPv4/BCMathCalculator.phpvendor/league/uri-interfaces/IPv4/NativeCalculator.phpvendor/league/uri-interfaces/QueryString.phpvendor/league/uri/UriTemplate.phpvendor/league/uri/Http.phpvendor/league/uri/HttpFactory.phpvendor/league/uri/Uri.phpvendor/league/uri/UriInfo.phpvendor/league/uri/BaseUri.phpvendor/league/uri/UriResolver.phpvendor/league/uri/UriTemplate/Expression.phpvendor/league/uri/UriTemplate/TemplateCanNotBeExpanded.phpvendor/league/uri/UriTemplate/Operator.phpvendor/league/uri/UriTemplate/VarSpecifier.phpvendor/league/uri/UriTemplate/Template.phpvendor/league/uri/UriTemplate/VariableBag.phpvendor/psr/http-message/src/ServerRequestInterface.phpvendor/psr/http-message/src/UriInterface.phpvendor/psr/http-message/src/StreamInterface.phpvendor/psr/http-message/src/UploadedFileInterface.phpvendor/psr/http-message/src/RequestInterface.phpvendor/psr/http-message/src/ResponseInterface.phpvendor/psr/http-message/src/MessageInterface.phpvendor/psr/http-factory/src/ResponseFactoryInterface.phpvendor/psr/http-factory/src/StreamFactoryInterface.phpvendor/psr/http-factory/src/UploadedFileFactoryInterface.phpvendor/psr/http-factory/src/UriFactoryInterface.phpvendor/psr/http-factory/src/ServerRequestFactoryInterface.phpvendor/psr/http-factory/src/RequestFactoryInterface.php
welcoMattic reacted with thumbs up emoji

@nyamsprod
Copy link

@nicolas-grekas I understand your hesitation about PSR-7 for the record I never used the PSR-7 aspect of the library. And the library predate PSR-7 UriInterface and UriFactoryInterface (which are in fact the only 2 interfaces used, since they are related to URI).

I only added PSR-7 so that the library could provide better interop with the PHP community (which is an underlying goal of the library). So if that was the one show stopper I believe then there's a case of moving that require dependency to an suggestion. But then again this would require a discussion before acting on that. As I stated

In other words, there are precedent for mutual participation/coordination in improving league/uri and Symfony.

So even opening a ticket to discuss this matter on the package would have IMHO clarify things or given a better context I supposed in your own decision making process.

But then again I want to completely stress out that I totally inderstand and respect your position/decision but I wanted to clarify this point.

wouterj reacted with thumbs up emoji

@ro0NL
Copy link
Contributor

ro0NL commentedMay 29, 2024
edited
Loading

we already use the library as a core dependency

noted, i tend to agree we should try to avoid using 2 URI libraries in core

perhaps we should invert the discussion a bit;

will symfony/uri be useful to league/uri? respectively being low-level and high-level URI libraries.

either way, i also tend to believe once symfony/uri is a thing, PSR-7 support will be requested sooner or later. which sounds fair.

OskarStark and alexandre-daubois reacted with thumbs up emoji

return $this->parameters;
}

public function __toString(): string
Copy link

@nyamsprodnyamsprodJun 4, 2024
edited
Loading

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

What is the expected returned value for this snippet ?

<?phpecho QueryString::parse('foo[bar]=1&foo[bar]=2'),PHP_EOL;
  • foo[bar]=1&foo[bar]=2
  • orfoo[bar]=2
  • orfoo[bar]=1

Copy link
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

I could not find an explicit explanation in the RFC about how to handle arrays in query strings. I went for the first solution, as demonstrated in the tests here:https://github.com/symfony/symfony/compare/4b37a248e96e13835678a8ae405931b3eb51af89..85eee4361f6e26db5a0fa26e5ddd13e3412228ae#diff-50079d0a06175c27cec11386ec6b7b0249844bdb99b036169ddb5ad49dc6ca03

@wouterj
Copy link
Member

wouterj commentedJun 4, 2024
edited
Loading

Thinking a bit more about this and reading the new comments here and inthephpleague/uri-src#137, I feel more and more like the proposed component is, at the moment, badly named and this makes it hard to discuss the matter.

If we want something that is compatible with URI specs and implements them correctly, I think we're at fault if we try to reinvent the wheel ourselves and should collaborate withleague/uri on this. Either us using that package directly instead of a new component, or Symfony adoptingleague/uri as a component - as suggested. I can't see how learning 2 specs from scratch and writing PHP code to comply with them is better than collaborating with people having the code and years of experience already.

However, if our goal is only to ease and standardize internal parsing of DSNs and not necessarily comply with the full URI specs, we should probably not useleague/uri but instead write a very simple class that only deals with "our DSN spec" (e.g. removeFragmentText and keep the:// simplification) and get back to calling the component Dsn. This clearly signifies "this is a mostly internal component to parse DSNs, and if you need to parse URIs in your application, please use another library".

@shadowhand
Copy link

@wouterj I understand where you are coming from, but "we just need DSNs" seems like avoidance to me. I would like to see this be a point of collaboration instead of fragmentation.

@wouterj
Copy link
Member

wouterj commentedJun 4, 2024
edited
Loading

@shadowhand yes, the first suggestion (2nd paragraph) is what I prefer as well.
But I have the feeling the second suggestion (3rd paragraph) is what is actually proposed in the diff of this PR. I hope highlighting this makes it easier to reason about it (as this discussion kept comparing complexity in this PR vs league/uri, which is an unfair comparison).

Btw, this is also the conclusion of a previous discussion about this when discussing a Dsn component:#36999

@shadowhand
Copy link

@wouterj I read through the various PRs that you referenced. It seems that:

  1. There is no consensus on what a DSN is in Symfony.
  2. An existing solution exists in the URI RFC, which will work formost existing DSNs.

Thus far there have been at least 3 distinct efforts to create a DSN/URI component and all have failed on this same point about whether or not DSN should be considered a URI (in the RFC sense).

@alexandre-daubois
Copy link
MemberAuthor

alexandre-daubois commentedJun 5, 2024
edited
Loading

There's undeniably a need for such a component, as the DSN parsing logic is present in multiple other components in the code and it comes with a lot of duplicated code with different support levels of features. This is why@wouterj's comment:

but instead write a very simple class that only deals with "our DSN spec" (e.g. remove FragmentText and keep the :// simplification) and get back to calling the component Dsn

is relevant to me. Let's remember that the main aim is to factorize DSN parsing logic into the Symfony codebase (with the auth decode thing), and that support for more general URI parsing is a "consequence" of this need. I'm curious to know what maintainers think about this?

I know that some components are already focused (initially I mean) to bring some features "internally", like PropertyInfo and PropertyAccess are for Serializer.

@nicolas-grekas
Copy link
Member

FTR, DSN parsing in each component as is done currently is fine to me. I'm not looking for a way to factorize DSN parsing, as this is not a domain on its own. Having some consistency in the way DSN are designed is nice, but isn't a requirement. DSNs are not the point here to me.

But I know we need a way to parse URLs that's free from PHP's oddities (listed in the description of this PR), and free from issues likephp/php-src#12703.

About the discussion related to league/uri, I'm listening carefully to the presented arguments. Still thinking about the topic.

alexandre-daubois and wouterj reacted with thumbs up emoji

@alexandre-daubois
Copy link
MemberAuthor

alexandre-daubois commentedJun 5, 2024
edited
Loading

Alright, thanks for clarifying. Right now, I'm able to fix the oddities that have been brought up without bringing complexity to the code. I think it worth challenging technically this PR to confirm if we can keep its simplicity while fixing what's been listed in the description. I still think that this approach, here, is worth it.

as this discussion kept comparing complexity in this PR vs league/uri, which is an unfair comparison

I don't agree with this point, I feel it is an important point both for maintainers and end-users. I personally think it is great to have the choice between multiple implementations as the end-user: some prefer to customize everything, some prefer more straightforward ways. I don't see this component and league/uri as competitors but rather as complementary, where you can choose which approach suits you best. This is why I think that comparing the complexity of those two packages is relevant.

@alexandre-dauboisalexandre-dauboisforce-pushed theuri-component branch 8 times, most recently from85eee43 toba5ed78CompareJune 6, 2024 08:45
@nicolas-grekas
Copy link
Member

Mate posted a proposal to add a native compliant URI parser to the engine!
https://externals.io/message/123997

This might close the topic in the best possible way :)

OskarStark, valtzu, and ro0NL reacted with thumbs up emoji

@alexandre-daubois
Copy link
MemberAuthor

Definitely, let's see where it goes!

@derrabus
Copy link
Member

How difficult would it be to turn the code of this PR into a polyfill for the proposed new URL extension?

@nyamsprod
Copy link

@derrabus a polyfill is not needed there's already a great implementation in PHP viahttps://github.com/TRowbotham/URL-Parser . But I would be really cautious about using that package by default or any package that just follow the WHATWG spec. I posted my remarks onhttps://externals.io/message/123997#124013

wouterj and alexandre-daubois reacted with thumbs up emoji

@derrabus
Copy link
Member

If we want to leverage that new extension, we will need a polyfill. Otherwise the extension will be unavailable to use until Symfony 9 possibly.

OskarStark reacted with thumbs up emoji

@Seb33300
Copy link
Contributor

e.g. ?foo=bar&foo=baz will return ['foo' => 'baz']). foo should be an array instead with the two values.

foo[]=bar&foo[]=baz should return an array but for mefoo=bar&foo=baz returning['foo' => 'baz'] is correct

@nicolas-grekas
Copy link
Member

nicolas-grekas commentedSep 10, 2024
edited
Loading

Let me close here, since we now have a plan: rely on php-src adding URI parsing. Let's try to have a polyfill instead.
Thanks@alexandre-daubois for all the work that happened here, and to@nyamsprod and everybody else involved in the discussion.

alexandre-daubois, valtzu, OskarStark, and welcoMattic reacted with thumbs up emoji

Sign up for freeto join this conversation on GitHub. Already have an account?Sign in to comment

Reviewers

3 more reviewers

@nyamsprodnyamsprodnyamsprod left review comments

@smnandresmnandresmnandre left review comments

@kelunikkelunikkelunik left review comments

Reviewers whose approvals may not affect merge requirements

Assignees

No one assigned

Labels

FeatureRFCRFC = Request For Comments (proposals about features that you want to be discussed)Status: Needs Review

Projects

None yet

Milestone

7.2

Development

Successfully merging this pull request may close these issues.

13 participants

@alexandre-daubois@ro0NL@nicolas-grekas@derrabus@wouterj@nyamsprod@OskarStark@Nyholm@shadowhand@kelunik@Seb33300@smnandre@carsonbot

[8]ページ先頭

©2009-2025 Movatter.jp