Skip to content

Add plugin client builder #158

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

Merged
merged 3 commits into from
Dec 16, 2019
Merged

Add plugin client builder #158

merged 3 commits into from
Dec 16, 2019

Conversation

Taluu
Copy link
Contributor

@Taluu Taluu commented Jan 6, 2019

Q A
Bug fix? no
New feature? yes
BC breaks? no
Deprecations? no
Related tickets fixes #96
Documentation None, for now ?
License MIT

What's in this PR?

This adds a plugin client builder, that allows to dynamically build a plugin client with plugins set on the go.

Why?

If you have some conditions on your plugin client (such as the auth can vary, some more headers can be dynamically added, ... like in the github's api from knp or my behat extension for http calls), it can be useful to not have a static plugin client with its plugin list, but to be able to add / remove them when needed.

As mentionned in the two repositories ([https://github.com/KnpLabs/php-github-api/blob/2.10.1/lib/Github/Client.php#L314-L331](Github's Client for the github php api of knp, or my behat extension), we both had to implement our own plugin builder, so this shows there's a "demand" for that.

We could have used the PluginClientFactory, but the problem with it is that when creating the client, we need to provide all the plugins on the go, which is not (IMO) practical.

Example Usage

use Http\Client\Common\PluginClientBuilder;

// $client is a php-http client or a psr-18 one
// $plugin is a php-http plugin, pick one (auth basic, auth oauth, ... anything goes)

$builder = new PluginClientBuilder();
$builder->addPlugin($plugin); // by default, priority is 0

// do stuff....

$pluginClient = $builder->createClient($client);
// plugin client is a PluginClient with the `$plugin` plugin

// ...

$pluginClient = $otherBuilder->createClient($client);
// plugin client is another PluginClient and not the same as the one built previously

Checklist

  • Updated CHANGELOG.md to describe BC breaks / deprecations | new feature | bugfix
  • Documentation pull request created (if not simply a bugfix)

To Do

  • Discuss the mutability ? decision : remove it.
  • How should we handle options in the builder ? In the create client ? In the constructor ? If in the create client, if changed should it retrigger the client creation ? decision : Add setter and remover for options, and inject that into the plugin client.
  • Add priorities in the plugins
  • Remove naming plugins
  • Tests ? As I didn't see many of those, and I wanted to throw a poc for a builder first

@Taluu Taluu force-pushed the builder branch 4 times, most recently from 3edf370 to 3358fa6 Compare January 6, 2019 21:47
Copy link
Member

@sagikazarmark sagikazarmark left a comment

Choose a reason for hiding this comment

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

Thanks a lot for the PR. I had a few comments above.

@Taluu
Copy link
Contributor Author

Taluu commented Jan 7, 2019

Updated with remarks from @sagikazarmark. I added the client in the constructor rather than the createClient to have a better "immutability" handling.

But with that, the options given in the createClient may pose problem if it changes... So we could either pass in into the constructor too, or find some sort of invalidating the built client.

Or we could add a setter for that one, as having options is, as its name implies it, optionnal.

I'm also not really convinced we should add a setter for the client though, I think the constructor should do the trick. Maybe adding a withClient($client) to get a new builder with the selected client ?

@sagikazarmark
Copy link
Member

Just to be clear: I don't think in this case immutability is a good idea. It my not even be practical. For example: the builder could be called by various parts of a DIC. If it's immutable, essentially every call to it would be ineffective. So I think it's okay to have a mutable state in the builder.

Also, what's the point of caching the client? Again, the builder might be called by a DIC in a terminal stage of the dep resolution process and then never used again. And as I said, the natural thing to expect from a function called createClient, accepting a client instance is to return one with my client inside, not an earlier cached one. I think it would be more confusing than useful. Creating multiple clients with different base clients can be a perfectly valid use case. So my second comment means that I think caching the client is not necessary, may even be harmful in some cases.

*/
public function __construct($client)
{
if (!$client instanceof ClientInterface && !$client instanceof HttpClient && !$client instanceof HttpAsyncClient) {
Copy link
Contributor

Choose a reason for hiding this comment

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

If this targeting master (v2) is there a reason why to keep the check for both ClientInterface and HttpClient ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If I understood @Nyholm, this is not specially for 2.0 (at least not blocking for it). I didn't check if there was a 1.x branch.

I'm not sure either if we should check for the ClientInterface though...

Copy link
Contributor

Choose a reason for hiding this comment

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

  • Master branch is v2
  • Yes there is a branch for v1...
  • Master branch which is v2 has as min requirement httplug: ^2.0 . The client on this version extends ClientInterface

I'm not sure either if we should check for the ClientInterface though...

  • Therefore my suggestion was to check only ClientInterface...

:)

Copy link
Contributor

Choose a reason for hiding this comment

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

Checking all three would make more sense for v1.

Copy link
Contributor Author

@Taluu Taluu Jan 7, 2019

Choose a reason for hiding this comment

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

I'll retarget if there's hope for a 1.x release then. @Nyholm @sagikazarmark ? If not, I'll remove the HttpClient, as the async client is not in the psr-18.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

why aren't you sure about that ?

Copy link
Contributor

Choose a reason for hiding this comment

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

ok sorry I haven't checked if plugin client accept an Async one.. it does..

Copy link
Contributor

Choose a reason for hiding this comment

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

for easier upgrade paths, version 2 accepts both PSR-18 clients and the old Httplug client interface. and the async client which only exists in Httplug.

Copy link
Contributor

@gmponos gmponos Jan 8, 2019

Choose a reason for hiding this comment

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

So my suggestion is not accurate? What am I missing? check this

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I removed the HttpClient, even though I'm also not sure due to @dbu's comment... I saw in the PR that introduced http-plug 2.0 (psr-18 compatible) that removing the compatibility for 1.0 was on purpose because it involved some magic.

So I should either target 1.x and thus support also the HttpClient, or leave it as is and remove it in here. But, AFAIC, new features should only target 2.0 (master), so....

@sagikazarmark
Copy link
Member

@Taluu I'm not against supporting this in 1.x as well per se, but 2.x is the new target of every feature now.

Copy link
Contributor

@dbu dbu left a comment

Choose a reason for hiding this comment

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

cool, thanks for the pull request!

i agree with mark that the builder should be mutable.

the plugin order is important. right now, we have addPlugin that appends at the end. what if we had prependPlugin and appendPlugin instead, to also add to the front?

we could do insertAfter and insertBefore additionally, but then the things using the builder would need to know about each other. or we could use a priorities - but that would collide with the name concept.
do we really need a remove method and implicit overwrite by setting the same name again? without remove, there would be no need for the name.

hm, another thing with overwriting: addPlugin adds at the end, except when you overwrite an existing name. if the name is already in the list, the overwritten plugin goes there.

@Taluu
Copy link
Contributor Author

Taluu commented Jan 8, 2019

I'll remove the immutability and the cache then. But for the plugin removal, it can be useful when we want to e.g remove a previous auth mechanism and replace it with another one. Unless the fact that a plugin in the bottom will somehow overwrite the previous one, I didn't check that case.

Adding appendPlugin and prependPlugin can be useful, I agree, but for insertAfter and insertBefore, I have some doubts, because as you said we would need to somehow know the plugins within the builder...

I'd rather add priorities actually. I'll try to work on that.

@sagikazarmark
Copy link
Member

Generally a builder serves one purpose. If you want for example create a base builder and use it later as the base for several other builders, you could add a cloneBuilder or new method that returns a new builder which can be modified without modifying a parent.

Priorities sound like and interesting feature. It can especially be useful when the building does not happen at one place (eg. the aformentioned DIC example). IIRC symfony event dispatcher has some sort of priority support.

Someone working on the HTTPlugBundle could jump in here as I imagine the bundle an similar solutions are first class citizens of this builder.

@Taluu
Copy link
Contributor Author

Taluu commented Jan 8, 2019

PR updated. I removed the immutability, and added priorities for plugins instead of names. I had to remove the possibility to remove a plugin, as we now have no means to remove a specific plugin (as there's no name given).

I chose to keep the fluent interface in the builder, as this is one of the few cases where it is legit. I'll update the first message with the examples.

Copy link
Contributor

@dbu dbu left a comment

Choose a reason for hiding this comment

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

great! i think with priorities its easier than with defined order and prepend/append insertBefore/insertAfter 👍

and i agree on the fluent interface for a builder.

i have some suggestions for documentation and commented on the options.

if the others agree with this design, we should have some tests to make sure priorities work as expected. and an entry in the documentation.

@Taluu Taluu force-pushed the builder branch 3 times, most recently from f9771be to 021b245 Compare January 10, 2019 13:43
Copy link
Member

@joelwurtz joelwurtz left a comment

Choose a reason for hiding this comment

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

Love it!

It just miss a test (like testing priorities)

@GrahamCampbell
Copy link
Contributor

What's the status of this PR?

@Taluu
Copy link
Contributor Author

Taluu commented Nov 17, 2019

IIRC, adding tests. I totally forgot about that one. 😂

@Taluu
Copy link
Contributor Author

Taluu commented Nov 23, 2019

Test added on the plugins priority check and the fact that the options should be passed onto the plugin client properly

@johnss
Copy link

johnss commented Dec 12, 2019

Builder in KnpLabs Github API allow remove plugin https://github.com/KnpLabs/php-github-api/blob/master/lib/Github/HttpClient/Builder.php
is there any strong reason to exclude remove plugin capability?

Copy link
Contributor

@dbu dbu left a comment

Choose a reason for hiding this comment

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

looks good to me. i think we can merge as is.

for the question of removing plugins: in general i am wary of such functionality, though the builder seems like the one place where it might be ok.

please help to understand the use cases: when would you need this? and how would you go about removing them? pass in the instance that should be removed? or remove by class with instanceof checks?

@Taluu
Copy link
Contributor Author

Taluu commented Dec 13, 2019

I'm also not in favor of "removing" stuff in a first pass. We could still consider it afterwards though.

@dbu dbu merged commit 0e6a80e into php-http:master Dec 16, 2019
@dbu
Copy link
Contributor

dbu commented Dec 16, 2019

thanks for the contribution!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add a new plugin to the PluginClient
7 participants