thephpleague / omnipay-common

Core components for the Omnipay PHP payment processing library
MIT License
329 stars 242 forks source link

Implement PSR-17 + PSR-18 #207

Open barryvdh opened 5 years ago

barryvdh commented 5 years ago

See https://github.com/thephpleague/omnipay-common/issues/204#issuecomment-474738658

Slightly breaking changes:

This should only affect people who are extending our Client. If they are providing a PSR-18/Httplug Client it should still work. Also the Requestfactory should support both the Httplug Requestfactory (deprecated) and PSR-17 RequestFactory.

barryvdh commented 5 years ago

cc @sagikazarmark Does this make sense to you?

barryvdh commented 5 years ago

@hacktivista I've added some things based on your PR:

And made the Body param work + test it. I want to drop diactoros when Guzzle (and other popular clients) provide the Factories by default, to avoid more difficult installs.

3ynm commented 5 years ago

About https://github.com/thephpleague/omnipay-common/blob/06cde89c5205fa26b3fa60e7bbe1f3b1fca795ff/src/Common/Http/Client.php#L89-L92

I think it's ok to accept body as string, but not accepting an stream is somewhat unexpected, don't you think? You can copy-paste this.

        if (!empty($body) && (is_string($body) || $body instanceof StreamInterface)) {
            if(is_string($body)) {
                $body = $this->getStreamFactory()->createStream($body);
            }
            $request = $request->withBody($body);
        }
barryvdh commented 5 years ago

Yeah we could allow a I guess.

barryvdh commented 5 years ago

Done, I only convert when it's a string, let the error fall through the factory otherwise.

judgej commented 5 years ago

Thanks, I'll take a look tomorrow - doing something very similar with Xero this evening, so I should have a bit more experience on this by the morning :-)

barryvdh commented 5 years ago

We could perhaps also deprecate this HttpClient and create a new PsrHttpClient with proper typehinting, and use that by default (starting with v3.1)

3ynm commented 5 years ago

@barryvdh here my version if you want some inspiration: https://gist.github.com/hacktivista/63cdd322e3764793f7885efdf91da76a

You could save it in the namespace Omnipay/Common so it becomes Omnipay/Common/HttpClient instead of Omnipay/Common/Client/Client

judgej commented 5 years ago

Can I just take a step back from this and ask a silly question?

I'm a bit confused about where discovery should sit. The idea behind factory discovery is that no specific implementation needs to be baked into a package, so it can use any implementation of some common HTTP functions that meet a standard (PSR) interface. That makes sense, and means the package (omnipay common) needs those factory implementations.

Now, the discovery is another beast. The documentation of the concepts behind these is pretty poor IMO, in that they don't suggest where they should sit. If they sit at the framework level, then the framework developer or framework user chooses just which discovery service they will use (e.g. http-interop, php-http, which I think have the same roles). The framework instantiate the factories then passes them into the packages that need them. That makes sense too, though it makes the factories a little more cumbersome to use without the framework around them.

So, we then have a hybrid. If I understand correctly, that means the package can accept factories if it is supplied with them, but will discover factories for itself if not. To do that teh package developer will have chosen a discovery package to use and that will be baked in, though I guess option (it is only used if factories are not instantiated outside of the package).

So - am I correct in that understanding? If so, is Omnipay going for the hybrid option, with php-http as the chosen fallback discovery package? Sorry for a will of text - I just need to understand how it all hangs together, and reading up widely has not actually touched on how these areas should be approache.

barryvdh commented 5 years ago

To be honest, I'm not really sure either.

Ideally, we (Omnipay Common) wouldn't need an Http class and we would just reference the PSR-18 Client Interface in the gateways. Same would go for the factories, the gateways could just reference that.

In a framework, I guess the PSR-18 Client + factories would be autowired or something, so that the correct one gets resolved. But for non-framework, you would have to define them and pass them to a constructor or something.

The problem right now is that there is no autowiring / dependency resolving in Omnipay, it's just passing the HttpClient and the HttpRequest, always, see https://github.com/thephpleague/omnipay-common/blob/c4567f786d283851be12319214ac7be7a0a0ee69/src/Common/GatewayFactory.php#L80-L88

That wouldn't be a problem if the PSR-18 Client alone would be enough to actually make a request, but to make a request you need the PSR-18 client, + a PSR-7 Request. And for that you would need at least a PSR-7 factory and a StreamFactory. So do we want to bother the Gateways with those specific details or not? Currently, that is all abstracted in our HttpClient, so gateways don't need to bother with the requests specifics.

Now the question, where does discovery belong. I do agree that if you want to follow DI principles, you'd rather not have that in this class, but perhaps a HttpClientFactory or a static method to set the default HttpClient.

Currently you would need to do something like this to set your own Client stuff:

use Omnipay\Omnipay;
use Omnipay\Common\Http\Client;

$client = new Client($psrClient, $requestFactory, $streamFactory);

// Setup payment gateway
$gateway = Omnipay::create('Stripe', $client);
$gateway->setApiKey('abc123');

But we could make it something universal:

use Omnipay\Omnipay;
use Omnipay\Common\Http\Client;

// Use own Http Client
$client = new Client($psrClient, $requestFactory, $streamFactory);
Omnipay::setHttpClient($client);

// Setup payment gateway
$gateway = Omnipay::create('Stripe');
$gateway->setApiKey('abc123');

Then we could use Omnipay::getHttpClient() to either get the defined version, or use discovery there to detect the factories/clients. And we could properly typehint the class (if we make a new one)

judgej commented 5 years ago

Switching to PSR-17+PSR-7 kind of changes the emphasis from configuring the client, to configuring the message. The client becomes a dumb "message pusher" - it's only got one method in the API anyway - sendRequest().

The way I have tackled it in a Xero package - wrongly or right as I am still experimenting - is like you describe. A PSR-18 client can be passed in, but it if isn't, then it will be auto-discovered. However, auto discovery uses a package I have chosen, since discovery of discovery libraries starts to go a bit inception. here my client is a decorator for the PSR-17 client, which can be passed in or also auto-discovered. This client can then modify the requests as they come through 9that would be middleware I guess) but being OAuth1 with token renewal, it also needs to invisibly generate new renewal messages and that involves a URL factory discovery too. This is a library I need for a project next week, but also I'm trying to learn about how PSR-7/PSR-17/PSR-15/PSR-18 hangs together in a practical way.

I'm rambling again, but it seems it is a good way to go for flexibility. If a layer could be built on top fo that to support the existing interfaces for Omnipay drivers, but also expose the underlying PSR-factories and client for new or updated drivers to use. I don't think the drivers should mess about with any discovery though - they should just say, "give me the PSR client" or "give me the PSR request factory". They are simple to use after that (though I have found including the PSR URI factory is essential to do this - not sure if there was an oversight in one of the PSRs allowing UriInterface or strings in some PSRs, but only UriInterface in others).


Just as a principle, even if a single discovery package was chosen for Omnipay Common, I think it:

barryvdh commented 5 years ago

Removing Discovery now would be a big breaking change, as people would need to update their code to make Discovery work.

judgej commented 5 years ago

Agreed, I'm not suggesting removing it. Just make it an option to install. The idea would be to use discovery built in to Omnipay Common, requiring an installation of a package, or do your own discovery or hard wiring and pass your factories in. In time, we can support multiple discovery packages with no breaking changes.

barryvdh commented 3 years ago

Right, for Guzzle7 we should probably merge this sometime.

sagikazarmark commented 3 years ago

cc @sagikazarmark Does this make sense to you?

Probably a bit late to the discussion, but yeah, makes sense 😄

matthewnessworthy commented 3 years ago

@barryvdh what would be required to get this merged and tagged? is there something I can help with?

barryvdh commented 3 years ago

Not sure if we need to do anything still. Was kinda hoping that guzzle/psr7 would provide the PSR-17 implementations, but that is only in 2.x and not yet released.

Do you have a breaking issue for now? I think Omnipay should still work with the new Guzzle etc, right?

matthewnessworthy commented 3 years ago

Guzzle 7 was not working for me in the 3.1@dev branch ( https://github.com/thephpleague/omnipay/pull/615#issuecomment-698159877 )

Axent96 commented 1 year ago

It looks a bit similar with https://github.com/thephpleague/omnipay-common/issues/204 && https://github.com/thephpleague/omnipay-common/issues/262 Any plans for progress here?