No code yet, but the way you currently have to redirect is more than ugly :)
In general, I think the result would be much cleaner if we move the responsibility of dealing with redirects up in the chain. A few ideas/thoughts:
- Allow to return a Redirect response in doExecutePayment().
- Allow to return a Redirect in $paymentType->resumeContext()
- - When called from doExecutePayment(), then it can simply be passed through by those).
- - In case of external payment methods, I think the common case is that you finalize/complete the payment in a custom controller to which you get redirect back, there it is very easy to return a response too
- And I think redirects should also be returned back to the caller of Payment->execute(). I assume those usually happen in the context of form (submits) and controllers, and those can deal easily with a response. And if it happens in a different context, then they can either resort to the current hack that you use in resumeContext() and if not, then it wouldn't work anyway right now either.
I think that would make the interaction of these different components cleaner, it would be easier to implement remote payment methods and it would open up ways for different kinds of user interfaces. For example, you could have a REST API where you need to deal with redirects in a different way (like returning an URL back as json or something).
Thoughts?
| Comment | File | Size | Author |
|---|---|---|---|
| #97 | payment_2339397_96.patch | 105.21 KB | xano |
| #93 | payment_2339397_93.patch | 102.44 KB | xano |
Comments
Comment #1
berdirComment #2
xanoYou're absolutely right that the current implementation of redirecting users needs to be improved. The reason no
RedirectResponseis returned, is because I figured payment execution may not always happen in a context that allows responses to bubble up to the controllers.Comment #3
berdirTrue, but I think that is a problem that the "payment executor" needs to solve, only he knows in which context he is running and what he can do there and what not. We can possibly document this hack somewhere as a workaround if there is no other way of doing it.
One pretty nasty issue I had with this is that this has weird side effects when done in a _content/_form route. Because Drupal will first fully render the page before the response will get replaced. And one side effect of that is that drupal_set_message()-messages will get rendered and then dropped, so any message that is added on that page is never displayed.
Comment #4
xanoThe payment type's context (a.k.a. "payment executor) can be embedded and not have control over the controller and the response. This is what happens with Payment for Drupal Commerce 7.x-1.x, for instance. Basically whenever the context is not the primary application within Drupal.
Comment #5
xanoI guess the payment type's context can then still translate a response to whatever its primary application supports.
I wonder if we can refactor
\Drupal\payment\Plugin\Payment\Method\PaymentMethodInterface::isPaymentExecutionInterruptive()to define whether the payment methodexecute()method will return a response or can be called 'inline' without interrupting the request, and if the payment type's context can then process *any* type of response that is given back.Comment #6
xanoWe must take care when allowing payment methods to return responses, because if they can return a response, there must be a request too. What if there isn't one?
Comment #7
berdirWhy must there be?
And yes, all valid points, but they all apply to the current situation at least as much?
I can't think of a situation where the caller of executePayment()/resumeContext() does not have at least as control over the page flow than the implementation, and if there is no way out, he can use the same hack as we now use inside them?
Comment #8
xanoBecause if there is no request (and payment method plugins can have that injected themselves, we don't need to pass it on), what will plugins return a response to?
Comment #9
xanoWhat about this? The only drawback I see is that with the old approach, payment methods defaulted to being interruptive (which usually means they redirect the user), to which payment type contexts could respond by executing payments in separate windows. Now, payment methods can say they return responses upon payment execution, but don't, or not return responses while they say they do.
Comment #11
berdirLooking more at this and with the recent core changes, returning a Url object might be the most flexible way now?
Something like this, completely untested and undocumented. Lots of unit tests will break, but they should be a lot easier to implement now too I guess?
Comment #13
xanoThat will be easier in most cases, but a redirect is a payment method implementation and I would not want to move that entire (and implementation-specific) responsibility up the chain.
Returning a response takes slightly more code, but does keep the approach fairly generic.
Comment #14
xanoIf those methods return responses, then they could theoretically also return responses that display an HTML form with some JavaScript to submit the form using POST, right?
Comment #15
berdirI switched to Url because of form redirects, which is used in one of the two cases. where we actually do something with the response of the whole stack. Those don't support response objects, and there isn't an obvious way to get the Url out of it again I think, at least not without reparsing it into a Url object
Yes, full responses might be more flexible in some ways, but Url objects are easier to handle and could be used in more contexts (like a REST request) I think. Not sure...
Comment #16
xanoCould you elaborate on this?
Returning a Url object for REST works only if the redirect uses GET rather than POST.
I am extremely careful with this, because other platforms have implemented these features in too specific ways in the past and in doing so imposed limitations on plugins and I don't intend on making the same mistake.
Alternatively, is there a way to get the controller for the current request and feed it a Response object directly, or is that the hack that Payment currently uses?
Comment #17
berdir(this is possibly not correct yet in case it returns NULL..)
This is now very easy if we return Url objects. But if we return redirect responses, then we have to parse this out of the response again to get a Url object. Or not use the form API for the redirection. Which would mean ugly hacks again.
Comment #18
xanoThat only supports a GET redirect. It's still perfectly possible to do anything else, but I don't want to communicate to developers that we support one type of payment execution more than we support others.
Would you know more about this?
Comment #19
xanoCompletely unfinished, but this builds upon my first patch to return responses.
Comment #23
xanoRe-roll.
Comment #25
xanoComment #27
berdirI don't get your point.
Yes, returning a response could theoretically support anything, but do you really want to theoretically support anything? I don't see how we can?
What about the form example? How do you intend to *properly* do a form submit redirect if you could get any kind of response back? Even getting it out of a RedirectResponse is tricky enough.
And on REST, returning a URL allows you to return that to the client so that he can parse it and go there himself.
What other than a GET redirect would you want to support? POST redirects don't really exist and you can return an actual page either.
Comment #30
xanoI think I am convinced that returning a
\Drupal\Core\Urlis useful. However, I would like to explore the options to also allow payment methods to return a response if not a web service, but a user requests payment execution. Is there a way to distinguish between such requests? We could also introduce aPaymentExecutionInterfaceand let implementations expose whether they can return a URL and/or a response, or nothing at all, but that could be considered overkill.Comment #31
berdirI think I'm still trying to understand what the use cases are for returning full-blown response objects.
Comment #32
xanoSuch responses could contain a form that does a POST redirect, for instance. It's slightly nicer than doing a GET redirect to such a form first.
Comment #33
xanoThis also means that any payment method that does not need to redirect payers can return the URL returned by PaymentType::resumeContext(), because we'd have to change that method here as well.
Comment #34
berdirYes, that is what I did in my earlier patch. The return value from resumeContext() is just passed through for payment methods that don't have their own redirection.
Comment #35
xanocan or can't?
Comment #36
berdirCan't, sorry.
Comment #37
xanoWell, if a controller is being used for the current request, then we can feed it a Response, right? So by default executing a payment returns nothing, but we will allow a Url to be returned for redirects. However, if there is a sane way to check if a controller is being used, we can let payment methods also return a Response.
Such Response objects can then contain a complete page, possibly with a form that automatically (using JS) POSTs to a third party URL. We can also easily do this by first returning a Url, redirect to another page on the site, and then POST a form to the remote server, but that would require an extra step. It's not very clean.
Comment #38
xanoI discussed this with @donquixote at DrupalCamp Ghent and we worked out the following idea:
PaymentMethodInterface::executePayment()returns NULL or aPaymentExecutionInterfaceobject. The object can contain a URL and aResponseobject that both allow calling code to continue the payment execution if necessary. Calling code can then decide to feed back the response to the controller, or to perform a GET redirect to the given URL.What do you think of this?
Comment #39
donquixote commentedI think we still have slightly different ideas, but Xano knows much more about the module and the use cases. Introducing the return object and the helper seems like a good direction. Once implemented on a few payment methods, we'll see if this can or should be improved.
EDIT: Main points I was that
- Payment plugins should focus on what distinguishes one payment method from another, and avoid redundant boilerplate. This is what the helper is for.
- The less details of the helper are exposed to payment plugins, the easier it will be to refactor the helper later.
These were my thoughts, all in the "DRY" direction. But I am not pushing for a specific solution.
(And now back to silently observing)
Comment #40
xanoThis is a partial patch for the approach discussed in the past few comments.
Comment #42
xanoComment #44
xanoThe patch from #42 introduces the concept of a payment execution status, which can have three 'values':
The patch uses booleans for 1 and 3, and an object for 2, which admittedly is somewhat inconsistent and if we continue using this approach, we'll want to fix this. The object that is available in case the execution is still in progress contains a URL at which execution can be resumes. An optional response may be available too, and if the payment type's context is able to pass it on to a controller, it can do so. However, only support for the URL and GET redirects is required.
In addition to all this, the patch also adds a resume entity operation, so payers can make use of this feature from within the UI. It's a little out of scope, but it illustrates why I added a new method for getting the payment execution status and it provides a default use case.
Comment #45
xanoMaybe we will want to change resume to complete, so we can use it for payments of which the owner never even started execution yet, as is the case with
\Drupal\payment_reference\Element\PaymentReferenceBase.Comment #46
xanoPaymentInterface::execute()will need to be refactored too.Comment #47
xanoComment #49
xanoComment #50
xanoThe last two patches should give you a good idea of why I wanted to try and support responses as well. Now we have a dedicated Complete operation for payments with a corresponding controller, this route can just return the payment's execution completion response. Because we can always build a
RedirectResponseon-the-fly as long as we have a URL (and we will always have one), any calling code can also assume that as long as there is a completion response, it can freely choose between using the URL directly or the complete response.Adding the Complete operation was not necessary for this issue, but it provides a good use case, as described above. It also shows that the fundamental changes we are making here contribute to supporting customers resuming payments they started earlier, but for which they aborted the execution workflow.
Comment #54
xanoJust a few unit test failures left.
Comment #56
xanoComment #58
xanoFixed all unit test failures and bugs they exposed.
Comment #59
xanoTeestboooot?
Comment #60
xano...
Comment #61
xanoAttempted test run #396...
Comment #62
xanoComment #64
xanoComment #66
xanoComment #69
xanoRe-roll.
Comment #71
xanoComment #72
xanoI cleaned up a bit based on my findings for #2372041: Provide a helper for form POST redirects.
Comment #74
xanoRe-roll.
Comment #75
xanoRe-roll.
Comment #76
xanoThis is the correct patch file.
Comment #79
xanoComment #82
xanoRe-roll.
Comment #83
berdirI'm still confused by this.
Having an object that can have either a response or a redirect URL seems to be very complex to support. How do you know which one to use when when you get such a response object? I've seen you for example also hardcode getRedirectUrl() in the form submit callback, what happens if a payment method returns a Response?
Also not sure about the separation of doExecutePayment() and getPaymentExecutionResult(), what do we gain from that? I'm not sure what I'm supposed to do now in which method. Our payment methods just build up a URL to redirect to, for now, I've built it in doExecutePayment(), stored it in a property and then I return it in the other method.. but why do I have to do that?
Comment #84
xanoIt MUST return a
\Drupal\payment\Response\ResponseInterfacewhich contains both a URL and a Symfony response. There MUST always (also) be a URL to redirect to. If the payment method provides no complex response of its own, it can return a\Drupal\payment\Response\RedirectResponse, which converts a URL to a Symfony response automatically. Calling code is allowed to either use the URL or the Symfony response from the, because redirecting to the URL or returning the Symfony response MUST have the same effect in terms of payment execution.\Drupal\payment\Plugin\Payment\Method\PaymentMethodInterface::getPaymentExecutionResult()exists so we can determine whether the payment execution process was completed or not. This allows us to resume the process in case the payer closed their browser window, for instance.\Drupal\payment\PaymentMethod\PaymentMethodBase::doExecutePayment()is a convenience method, so plugins won't have to deal with all the logic in\Drupal\payment\PaymentMethod\PaymentMethodBase::executePayment().Comment #85
xanoAdditionally:
If the redirect URL is unique to the payment and cannot be computed, you must indeed store it somewhere (likely the plugin configuration) so it can be returned at any time. You will only have to return it in
\Drupal\payment\Plugin\Payment\Method\PaymentMethodInterface::getPaymentExecutionResult(), not in any other methods.Comment #86
berdirYes, the URL is computed, based on the payment. I don't need to persist it, but until now, we built that URL and redirected with the current subscriber pattern, in doExecutePayment(). And now the question is in which method I am supposed to build that URL. Doing it in a method that is just supposed to return the result seems weird.
The public method already calls both methods. Who would call getPaymentExecutionResult() without first calling executePayment()? Is it even possible? Because right now in my implementation, it is not, and it is also not possible to call it later on.
doExecutePayment() might be a convience method, but it is a *required* convenience method, and I must save the payment there or things do not work. So again, why not just return a result from doExecutePayment()?
Also, why wrap the Response object in a PaymentExecutionResult? The idea of having such an object seems useful, but why wrap it twice?
Comment #87
xano\Drupal\payment\Controller\CompletePaymentdoes this in the patch in order to let payers continue a payment after the execution process was aborted. This can, of course, only be done if the payment method supports the continuation of the payment's execution. If it doesn't, it does not have to return a response/URL and it can simply say execution was completed.If you have suggestions for making saving payments easier, I'd be happy to hear them. I feel we have to impose this responsibility either on payment type or payment method plugins, but I may well be wrong about that.
Why would it be easier to let
doExecutePayment()return the result?executePayment()is the API method and the default implementation already takes care of returning the result. As illustrated by\Drupal\payment\Controller\CompletePaymentwe may need the result in other places, which is why we need a dedicated method.I did that, because responses are now also used by payment types. See
\Drupal\payment\Plugin\Payment\Type\PaymentTypeInterface::getResumeContextResponse(). Can you suggest a way to make this simpler, without mixing things up too much?Comment #88
xanoFeedback from my IRC talk with @Berdir:
\Drupal\payment\PaymentMethod\PaymentMethodBase::doExecutePayment()non-abstract, because not all payment methods will need it anymore.\Drupal\payment\PaymentMethod\PaymentMethodBase::executePayment().\Drupal\payment\Response\Responseand\Drupal\payment\Response\RedirectResponse, so calling code can simply pass on a URL that will be converted to a redirect response automatically.Comment #89
xanoComment #93
xanoRe-roll.
Comment #94
berdirTested the latest patch, works well for me.
You mentioned you wanted to change something about the Result objects? If you keep them, I would still suggest to have a helper buildResultWithUrl() or similar method in PaymentMethodBase.
Comment #95
xanoA method like that would return a URL if payment execution is still in progress, and NULL if it has been completed? That means we'll have two ways on the same class to get this information:
I can't help but think this sounds rather inconsistent.
Comment #97
xano@berdir and I talked on IRC and decided will will discuss possible small improvements in follow-ups, so we can already start making use of the new approach.
Thanks @berdir and @donquixote for providing useful feedback!