-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
New DSN component #36999
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 our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
New DSN component #36999
Conversation
20f51d1
to
9d40c78
Compare
c9a3830
to
d01c068
Compare
Thank you for working on this. I haven't read the code yet, but I think |
…arseSimple() to DsnParser::parse()
Thank you for the suggestion. I've been considering about doing that after I saw that parsing strings without functions will be way more commonly used. I've updated the PR and description accordingly. |
Tobias, thanks for working on this proposal. I haven't looked at the code yet either ... but the
|
Yes. I was not a big fan of the word “simple” either. I’ve renamed it after Fabien’s suggestion. If we are using only one “parse” method then there are two scenarios: B) we will always return a DsnFunction and for scenarios where you don’t support functions (which is “most” in symfony/symfony) you need to check if the function name is “dsn” and throw an exception of it isn’t. A dsn string is per definition (that I concluded and wrote down) recursive. That is why the value object needs to be recursive too. With other words: using a “getFunctions()” will not make any sense. It will always be zero or one items returned and there is not much you can do but calling “getMostOuterFunction()” and then ask for the values for one function at the time. |
Thanks for working on this Tobias ! |
@Nyholm let me share some code samples. Would this work? I know the DSN structure: // no functions
$dsn = DsnParser::parse('scheme://127.0.0.1/foo/bar?key=value');
$host = $dsn->getHost();
// single function
$dsn = DsnParser::parse('foo(scheme://127.0.0.1/foo/bar?key=value)');
$host = $dsn->getFunction()->first()->getHost();
// multiple functions
$dsn = DsnParser::parse('foo(bar(scheme://127.0.0.1/foo/bar?key=value))');
$host = $dsn->getFunction()->first()->getFunction()->first()->getHost(); I don't know the DSN structure: if ($dsn->hasFunctions()) {
do {
$dsn = $dsn->getFunction()->first();
} while ($dsn->hasFunctions());
}
$host = $dsn->getHost(); |
Thank you for a clear example. That works, but there is not type safety. In the current state of the PR we got: DsnParser::parse(string $dsn): Dsn
DsnParser::parseFunc(string $dsn): DsnFunction To implement your change, we need to: /**
* @return Dsn|DsnFunction
*/
DsnParser::parse(string $dsn) Since a DSN is most likely a user input, we do not know how it looks like. We only know if we expect "zero functions" or "zero or more functions". |
Dsn and Function shares few attributes (scheme and querystring, and maybe path) |
* | ||
* @throws SyntaxException | ||
*/ | ||
public static function parseFunc(string $dsn): DsnFunction |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
shouldn't that return Dns|DnsFunction
instead of a fake function named dsn
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That would force the user to always check what object they got back.
$dsn = DsnParser::parseFunc($userInput);
if ($dsn instanceof DsnFunction) {
// Handle function logic
// Foreach (arguments as argument)
// handle single argument
} else {
// Handle single argument
}
But with constant return types:
$dsn = DsnParser::parseFunc($userInput);
// Foreach (arguments as argument)
// handle single argument
Here's a different proposal. What if we forget about "functions"? Don't forget that "DSN as a general purpose config" is something that Symfony (probably) has invented. If you Google around, you can't find many DSN used like these. Others only use DSN for databases. So, instead of this:
Why not do this:
Parser would return either a This new format would simplify defining for example different failover mechanisms, etc:
|
I've been considering this too. But they dont not share these attributes. |
Thank you @javiereguiluz. Skipping functions would make things more simple.
Returning an object or an array still means that you always need to verify the type that was returned. The new format is maybe something we can work with. It is not as simple and easy for the users, but it might work. But how do you handle nested functions in an intuitive way?
|
DSN means Data Source Name, as pointed out by @csarrazi, it's not a standard and there are a lot of DSN formats used in the wild, mostly for historical reasons. However, as you pointed out, there is a well known and very popular standard for identifiers and structured identifiers. It's RFC 3986 (URI).
URIs are a subset of IRIs (a URI can be dereferenced, a IRI may not be dereferencable, for instance Most modern DSN are in fact URIs. URIs and IRIs are at the core of the Web and are also used as identifiers for non-Web resources (XML/JSON-LD namespaces, UUIDs, ISBN, EAN). As Symfony is committed to implement open standards, I think that we should only use valid URIs as identifiers in Symfony (it also allows to parse and build them from most programming language, as there are libraries to manipulate URIs in all ecosystems). To me we shouldn't create a DSN component, but a URI (or IRI if we're ambitious) component. PHP has builtin function to manipulate URIs. There also is https://uri.thephpleague.com/ which is pretty good. To create such component, there two issues main to solve:
|
For instance, here you can see how @jderusse used the URI standard to allow passing advanced options in the Mercure's transport DSN: https://mercure.rocks/docs/hub/config#bolt-adapter |
Thank @dunglas. I've done much of the same research as you just showed. I tried to add a section (see "Implementation" in PR description) to sum up what this component is really doing. From a specification point of view, this component builds on the URI specification (RFC 3986) and adds support for functions and If we decide that Symfony should stop using those 2 things, then I agree that this component should not be merged. We should rather use |
@javiereguiluz the URI spec reserves a lot of delimiters and sub-delimiters for this kind of use cases:
https://tools.ietf.org/html/rfc3986#section-2.2 Example:
https://tools.ietf.org/html/rfc3986#section-3.3 For instance, @Nyholm the builtin PHP functions are a bit low level (and don't cover 100% of the spec) and I'm not sure that's a good idea to rely on an external component for something as low level as parsing our own DSNs. So IMHO it makes sense to add a URI component in Symfony. And it could be useful for a lot of other projects (for instance API Platform use URIs and IRIs a lot, and we would benefit from such component too). |
My main argument against a DSN component is that many DSN formats are not compatible with the format used as an example in RFC 3986. For example, here are DSNs for database access for various languages:
As you can see, this is both vendor and library-dependent. I do think that it would make sense to have a URI component, which could be reused for some DSN implementations, but not a DSN component per se. It is also important to note that some database providers try not to expose any credentials-related information in their DSNs, which are often only named connections, which are fetched from various places (tnsnames.ora, ODBC manager, etc.). |
I'll comment these one by one.
Supported by the DSN component.
Not supported, but
Not supported
Not supported
Not supported
Supported by the DSN component.
Supported by the DSN component.
Supported by the DSN component, but could be added if we feel it is important |
I'm not sure I agree about an Uri component. The domain looks pretty different to me and this would restrict the usefulness of the component a lot. Actually, I would like to suggest adding connection factories to the Dsn component. We're already using the Cache component as a dsn-to-connection factory: to me that's a big part of the reusability need. We could also benefit from a connection registry, indexed by dsn; and from providing a lazyness mechanism that would prevent requiring proxy-manager for dealing with this concern. My 2cts :) |
I have an edge case for tests; MongoDB connection URIs: https://docs.mongodb.com/manual/reference/connection-string/ Specifically, they can list multiple hosts and duplicate querystring parameters. As for actually using this component in the context of mongodb; since mongodb drivers accept a uri already, there is no reason to parse one. The string is simply passed directly to the driver. |
I think we need to take a decision here. Do we want a generic DSN component or are we happy with the current situation? |
My 2 cents: we're probably not happy with the current situation (too many different parts of Symfony using DSN) ... but we aren't 100% happy with a DSN component either because it looks like there's a lot of different possible DSN formats, so abstracting is hard. |
I do agree with @csarrazi & @dunglas that it makes more sense (to me) to have a URI component. This is a known format with its RFC (3986) and is very likely what we should push forward for our own DSNs. If we go towards a generic DSN component we will have no reason for refusing the likes of ODBC MS and that can quickly become very bloated for very particular edge cases. |
I forgot about the URI idea. I think having a URI component would solve this issue nicely (at least as a first step). |
What about a DSN contract, and a DSN component with several implementations that are reused across several components? For example: If some package needs a custom Dsn then it can live in his own package, implementing the contract from the package, and not requiring the implementations package. Maybe this could be a good way of implementing for example ODBC edge cases in his very own class. If I need something not standard at all I still can implement the contract with my very own class. I don't know if almost every DSN can be parsed using regular expressions, or if a lexer could be the option to help each DSN implement his custom parser. Also, there could be a factory that takes an array of DSN implementations in his constructor. Then you could pass it a DSN, and try to guess which specific implementation is by iterating all the registered classes and returning the one which matched first. I'm unsure if this last idea could be possible, but this is a rough example: $dsnFactory = new Symfony\Component\Dsn\Factory([
Symfony\Component\Dsn\PdoDsn::class,
Symfony\Component\Dsn\RedisDsn::class,
Symfony\Component\Dsn\OdbcDsn::class,
Symfony\Component\Dsn\UriDsn::class,
Symfony\Component\Dsn\MailerDsn::class,
Symfony\Component\Dsn\SymfonyDsn::class,
App\Dsn\MyCustomDsn::class,
]);
$dsnFactory->create('odbc:Driver={Microsoft Access Driver (*.mdb)};Dbq=C:\\db.mdb;Uid=Admin'); // Symfony\Component\Dsn\OdbcDsn instance If this is possible, the DSN contract may have a static |
Thank you for all the reviews and feedback. I see that we are not convinced that this PR should be merged. I will release this PR as version 2.0 of nyholm/dsn. |
A few years ago I created nyholm/dsn. This PR is building on to the work started in that library.
I've seen discussions in about a new DSN component started by @Simperfit. He points out that 7 components is using some form of DSN. I've also seen @jderusse PR in #24267 where he creates a specific DSN factory class for each component.
What I found interesting is @csarrazi's comment (#32546 (comment)) saying "there is not clear spec for DSNs". I have also not found a definition myself so I thought I would start by defining that a DSN is. I looked on all uses of DSN in symfony/symfony. I later broaden my search to DSN in PHP and continued to other languages. I came up with the following definition based on RFC 3986:
(See full definition in the docs)
Please correct me if I'm wrong and there is a DSN RFC somewhere.
Implementation
As you may see from the implementation or from how Symfony is using DSNs, the new component is basically just a bit more advanced
parse_url()
with a few benefits:Documentation
I've started a page that could be converted to an PR to symfony/symfony-docs. I'll refer to that page as detailed PR description. See documentation.md.
Usage
There are 2 methods for parsing;
DsnParser::parse()
andDsnParser::parseFunc()
.The latter is for in situations where DSN functions are supported.
If functions are supported (like in the Mailer component) we can use
DsnParser::parseFunc()
:Implementation in existing components
I've started implementation on Mailer and Messenger component. As you can see it will not remove loads of complexity from the component's logic. It just removes the parsing logic, you will still need to check if ie the "port" is set or not.
I've also started to implement this on the Redis Cache adapter. I stopped half way because there was not much logic removed and the diff was pretty much 1:1.
Where I believe this component would be helpful for the tests for any components. There is no longer any need to test the parsing of different DSNs.
Related PRs
This PR will fix #32546
It will also make it simple to fix:
Future work
I've been working on this PR time to time the last week. I would like to get more feedback and comments on the current implementation.
Maybe the value objects could possibly be improved by adding default values to the getters?