Place Order Service API for Hyvä Checkout
The Place Order Service (POS) API controls how orders are placed in Hyvä Checkout. Unlike Luma checkout where payment methods handle order placement, Hyvä Checkout centralizes this responsibility, giving merchants flexibility to position payment methods anywhere in the checkout flow while maintaining consistent order placement behavior.
What is a Place Order Service?
A Place Order Service is a PHP class that controls the order placement workflow for a specific payment method. Each payment method can have its own custom POS, or fall back to the default implementation at \Hyva\Checkout\Model\Magewire\Payment\DefaultPlaceOrderService.
The POS handles:
- Order creation - Converting the quote to an order
- Redirect control - Determining where customers go after order placement
- Post-order actions - Executing frontend JavaScript via the Evaluation API (e.g., 3DS authentication modals)
- Exception handling - Managing errors during the order placement process
Why Use a Custom Place Order Service?
The default Place Order Service works for simple payment methods that don't require post-payment actions. Create a custom POS when you need to:
- Prevent automatic redirect after order placement to show additional UI (e.g., 3DS authentication)
- Execute frontend JavaScript after the order is created using Evaluation results
- Control the redirect destination based on payment-specific logic
- Handle payment method-specific exceptions with custom error messages
Since version 1.1.13, the POS integrates with the Evaluation API, allowing backend PHP code to trigger frontend JavaScript actions before or after order placement.
How the Place Order Service Works
The order placement flow in Hyvä Checkout follows these steps:
- Customer clicks "Place Order" - The
\Hyva\Checkout\Magewire\Maincomponent receives the request - POS Processor retrieves the service - The processor queries
PlaceOrderServiceProviderfor a POS matching the quote's payment method - Fallback to default if needed - If no custom POS exists,
DefaultPlaceOrderServicehandles the order - Order is placed - The POS creates the order and assigns the order ID
- Evaluation results execute - Any post-order Evaluation results (redirects, validations, modals) run on the frontend
- Customer is redirected - Based on
canRedirect()andgetRedirectUrl()return values
Let Hyvä Checkout Handle Order Placement
Do not convert quotes to orders inside payment method components or PSP callbacks. Hyvä Checkout's flexible step ordering means payment methods may not be on the final step. If a quote converts to an order mid-checkout, subsequent steps cannot access required quote data.
Always use the Place Order Service for order creation.
Creating a Custom Place Order Service
Example: 3DS Authentication Flow
This example demonstrates a payment method requiring 3DS authentication after order placement. The workflow:
- Order is placed successfully
- A modal displays for 3DS authentication
- Customer completes authentication
- Customer is redirected to the success page
What is 3DS Authentication?
3D Secure (3DS) is a security protocol for online card payments. After submitting payment details, the cardholder's bank verifies their identity through a password, SMS code, or biometric check. Successful authentication allows the transaction to proceed; failure declines it. 3DS reduces fraud and often shifts liability from merchants to issuing banks.
Step 1: Create the Place Order Service
Extend AbstractPlaceOrderService to implement only the methods you need to customize. The abstract class provides default implementations for all other methods.
<?php
// File: My/Example/Model/Payment/PlaceOrderService/FooPlaceOrderService.php
namespace My\Example\Model\Payment\PlaceOrderService;
use Hyva\Checkout\Model\Magewire\Payment\AbstractPlaceOrderService;
use Hyva\Checkout\Model\Magewire\Component\Evaluation\EvaluationResultInterface;
use Hyva\Checkout\Model\Magewire\Component\EvaluationResultFactory;
class FooPlaceOrderService extends AbstractPlaceOrderService
{
/**
* Prevent automatic redirect after order placement.
* The 3DS modal must display before redirecting.
*/
public function canRedirect(): bool
{
return false;
}
/**
* Define post-order evaluation results.
* Called after the order is successfully placed.
*
* @param EvaluationResultFactory $resultFactory Factory for creating evaluation results
* @param int|null $orderId The placed order ID, or null if order placement failed
*/
public function evaluateCompletion(
EvaluationResultFactory $resultFactory,
?int $orderId = null
): EvaluationResultInterface {
// Always prepare the redirect to success page
$redirect = $resultFactory->createRedirect('checkout/onepage/success');
// If no order was created, just redirect (handles edge cases)
if ($orderId === null) {
return $redirect;
}
// Create a validation that triggers the 3DS modal on the frontend
// The validator name 'foo-authentication' must match the registered frontend validator
$validate = $resultFactory->createValidation('foo-authentication');
// If authentication fails, still redirect to success (order is already placed)
$validate->withFailureResult($redirect);
// Create a navigation task that executes the redirect after validation completes
$navigationTask = $resultFactory->createNavigationTask('foo-redirect', $redirect);
// executeAfter() required before version 1.1.18
$navigationTask->executeAfter(true);
// Return a batch that executes validation first, then redirect
return $resultFactory->createBatch()
->push($validate)
->push($navigationTask);
}
}
Step 2: Register the Place Order Service
Map the custom POS to the payment method code in di.xml. The item name must match the payment method code exactly.
<!-- File: etc/frontend/di.xml -->
<type name="Hyva\Checkout\Model\Magewire\Payment\PlaceOrderServiceProvider">
<arguments>
<argument name="placeOrderServiceList" xsi:type="array">
<!-- Item name must match the payment method code -->
<item name="foo" xsi:type="object">
My\Example\Model\Payment\PlaceOrderService\FooPlaceOrderService
</item>
</argument>
</arguments>
</type>
Step 3: Create the 3DS Authentication Modal
The modal must be placed outside the Main component because payment methods may not be visible when "Place Order" is clicked. Use the hyva.checkout.init-validation.after container to ensure proper DOM placement.
<!-- File: view/frontend/layout/hyva_checkout_index_index.xml -->
<referenceContainer name="hyva.checkout.init-validation.after">
<block name="modals.three-ds.authentication"
template="My_Example::modals/three-ds/authentication.phtml"
/>
</referenceContainer>
Modal Placement
Do not place modal HTML inside the payment method template. Payment methods may be on a different step than the "Place Order" button. Always place modals in a container outside the Main component.
The modal template uses a Hyvä modal component. The example below is CSP-compliant, using method references instead of inline expressions.
<!-- File: view/frontend/templates/modals/three-ds/authentication.phtml -->
<div>
<div x-data="initFooAuthentication">
<div x-cloak
x-bind="overlay"
class="fixed inset-0 flex items-center justify-center text-left bg-black bg-opacity-50">
<div x-ref="dialog"
role="dialog"
aria-labelledby="foo_three-ds_authentication_dialog-label"
class="inline-block max-w-2xl mx-4 max-h-screen overflow-auto bg-white shadow-xl rounded-lg p-6 text-gray-700">
<div class="w-full font-medium text-gray-700 not-required">
<label for="authentication_code">Authentication Code</label>
<div class="flex items-center gap-4">
<!-- x-model is NOT CSP compliant; use x-bind:value + x-on:input instead -->
<input type="text"
x-bind:value="code"
x-on:input="updateCode"
id="authentication_code"
class="form-input"/>
</div>
</div>
<div class="flex gap-y-2 md:gap-x-2 mt-6 w-full">
<!-- x-on:click with method reference is CSP-safe -->
<button type="button" class="btn btn-primary" x-on:click="ok">
Confirm Payment
</button>
</div>
</div>
</div>
</div>
</div>
Step 4: Register the Frontend Validator
The frontend validator connects the backend evaluation result to the modal UI. The validator name must match the createValidation() name from the Place Order Service. The Alpine component must be CSP-compliant.
<!-- Add to the same template file or a separate phtml -->
<?php
/** @var \Hyva\Theme\Model\HyvaCsp $hyvaCsp */
?>
<script>
/**
* Alpine CSP-compatible component for 3DS authentication modal.
* Uses method references instead of inline expressions.
*/
function initFooAuthentication() {
return Object.assign(
{ code: null },
hyva.modal(),
{
/**
* CSP-safe replacement for x-model.
* Called via x-on:input="updateCode" on the input element.
*/
updateCode(event) {
this.code = event.target.value;
},
/**
* Initialize event listeners for modal display.
* Called via x-init="initialize()" (CSP-safe method reference).
*/
init() {
window.addEventListener('foo:authenticate:show', event => {
this.show().then(result => {
this.$dispatch('foo:authenticate:confirm', {
result: result && this.code === '1234'
});
});
});
}
}
);
}
window.addEventListener(
'alpine:init',
() => Alpine.data('initFooAuthentication', initFooAuthentication),
{once: true}
)
// Wait for the Evaluation sub-namespace to initialize
window.addEventListener('checkout:init:evaluation', () => {
// Register validator matching the backend createValidation('foo-authentication') name
hyvaCheckout.evaluation.registerValidator('foo-authentication', element => {
return new Promise((resolve, reject) => {
// Dispatch event to show the authentication modal
window.dispatchEvent(new Event('foo:authenticate:show'));
// Wait for the modal to dispatch confirmation result
window.addEventListener('foo:authenticate:confirm', event => {
event.detail.result ? resolve() : reject();
});
});
});
});
</script>
<?php $hyvaCsp->registerInlineScript() ?>
The complete flow: order is placed → evaluateCompletion() returns validation + redirect batch → frontend executes validation → modal shows → customer authenticates → validator resolves → navigation task executes redirect.