Checkout process review and implementationEdit on GitHub
To use the checkout in Yves, you need to configure it correctly and provide dependencies. Each step can have a form, a controller action, the implementation of the step logic, and a Twig template to render the HTML.
- Forms—current step form collection.
- Controller action—the action that is called when the step is triggered.
- Step—a class that implements the StepInterface and handles the data passed through the form.
- Twig template—template where the form is rendered.
Each form in the checkout uses
QuoteTransfer for data storage. When the data is submitted, it’s automatically mapped by the Symfony form component to
QuoteTransfer. If the form is valid, the updated
QuoteTransfer is passed to the
Step::execute() method, where you can modify it further or apply custom logic. There is also a Symfony Request object passed if additional or manual data mapping is required.
There are a few factories provided for checkout dependency wiring:
FormFactory—creates form collections for each step.
StepFactory—creates the steps together with their dependencies and plugins.
CheckoutFactory—where the StepProcess is created for all steps.
We have a step process that contains the list of the steps created. The
CheckoutProcess::process(Request, FormCollectionHandlerInterface) process method accepts
Request, which is currently submitted, and the
FormCollectionHandlerInterface implementation that contains the list of forms that are used in the current step. If multiple forms are used,
FormCollectionHandler selects the right one when the request is performed.
In most cases, forms need external data to work with. For example, the Checkout Address step uses a data provider to retrieve all stored customer addresses and company business unit addresses if a customer is a company user.
Another example is the Checkout Payment step. It uses a data provider to retrieve all available payment providers that are being rendered on the page.
Using a data provider is the only way to pass any external data to the form and step page that can be accessed, processed, and later saved into
Each data provider is passed when
FormCollection is created. When the handle method is called, the
FormCollection handler creates all form types and passes the data from data providers to them.
Checkout quote transfer lifetime
When a process or
postCondition() is called on
StepProcess, it tries to get the current valid step by walking through the stack of steps and calling
postCondition() for each, starting from the first in the stack. If
postCondition() fails, then this step is used for later processing. After that, the view variables with
QuoteTransfer and form are passed to Twig, and the HTML is rendered.
Postcondition is an essential part of the Processing step. It indicates if a step has all the data that it needs and if its requirements are satisfied. You can’t access the next step from the stack if the previous step’s postconditions are not met, but you can navigate to any step where postconditions are satisfied (
Postconditions are called twice per step processing:
- To find the current step or if we can access the current step.
execute(), to make sure the step was completed and that we can continue to the next step.
Postcondition error route
Inside your step, you can set a postcondition error route if you need to redirect to another error route than the one specified during the step construction.
How the quote transfer is mapped inside forms
Symfony forms provide a mechanism to store data into objects without needing manual mapping. It’s called Data transformers. There are a few important conditions required to make this work. Because you are passing the entire
QuoteTransfer, the form handler does not know which fields you are trying to use. Symfony provides a few ways to handle this situation:
- Using property_path configuration directive. It uses the full path to object property you are about to map form into—for example,
payment.paypalmaps your form to
QuoteTransfer:payment:paypal; this works when the property is not on the same level and when you are using subforms.
- Using the main form that includes subforms. Each subform has to be configured with the
data_classoption, which is the FQCN of the transfer object you are about to use. This works when the property is on the top level.
Checkout form submission
On form submission, the same processing starts with the difference that if form submit is detected, then the validation is called:
- If the form is invalid, then the view is rendered with validation errors.
- If form data is valid, then
execute()is called on the step that executes the step-related logic.
For example, add the address to
QuoteTransfer or get payment details from Zed, and call an external service.
You decide what to do in each
execute() method. It’s essential that after
execute() runs, the updated returned
QuoteTransfer must satisfy
postCondition() so that
StepProcess can take another step from the stack.
Normally each step requires input from the customer. However, there are cases when there is no need to render a form or a view, but some processing is still required, that is,
EntryStep. Each step provides the implementation of the
StepProcess calls this method and reacts accordingly. If
requireInput() is false, after running
postConditions must be satisfied.
Precondition and escape route
Preconditions are called before each step; this is a check to indicate that the step can’t be processed in a usual way.
For example, the cart is empty. If
preCondition() returns false, the customer is redirected to
escapeRoute provided when configuring the step.
External redirect URL
Sometimes it’s required to redirect the customer to an external URL (outside application). The step must implement
StepWithExternalRedirectInterface::getExternalRedirectUrl(), which returns the URL to redirect customer after
execute() is ran.
Each step must implement
Placing the order
After the customer clicks the submit button during
PlaceOrderStep is started. This step takes
QuoteTransfer and starts the checkout workflow to store the order in the system. Zed’s Checkout module contains some plugins where you can add additional behavior, check preconditions, and save or execute postcondition checks.
Zed’s Checkout module contains four types of plugins to extend the behavior on placing an order. Any plugin has access to
QuoteTransfer that is supposed to be read-only and
CheckoutResponseTransfer data object that can be modified.
PreCondition—is for checking if the order satisfies predefined constraints—for example, if the quantity of items is still available.
OrderSavers—is for saving the order. Each plugin is responsible for collecting certain parts of the order (sales module saves items, discount module saves discounts, product option module saves options). Each
OrderSaverplugin is wrapped into a single transaction; if an exception is thrown, the transaction is rolled back.
CheckPostConditions—is for checking conditions after saving, the last time to react if something did not happen according to plan. It’s called after the state machine execution.
PostSaveHook—is called after order placement, and sets the success flag to false, if redirect must be headed to an error page afterward.
Checkout response transfer
isSuccess(boolean)—indicates if the checkout process was successful.
- errors (
CheckoutErrorTransfer)—list of errors that occurred during the execution of the plugins.
isExternalRedirect(boolean)—specifies if the redirect, after the successful order placement is external.
redirectUrl(string)—URL to redirect customer after the order was placed successfully.
SaveOrderTransfer)—stores ids of the items that OrderSaver plugins have saved.
Checkout error transfer
errorCode(int)—numeric error code. The checkout error codes are listed in the following section Checkout error codes.
Checkout error codes
4001—customer email already used.
4003—cart amount does not match.
Save order transfer
idSalesOrder(int)—The unique ID of the current saved order.
orderReference(string)—An auto-generated unique ID of the order.
ItemTransfer)—The saved order items.
There are already some plugins implemented with each of those types:
CustomerPreConditionCheckerPlugin—checks if the email of the customer is already used.
ProductsAvailableCheckoutPreConditionPlugin—check if the items contained in the cart are in stock.
OrderCustomerSavePlugin—saves or creates a customer in the database if the customer is new or the ID is not set (guest customers are ignored).
SalesOrderSaverPlugin—saves order information and creates
ProductOptionOrderSaverPlugin—saves product options to the
DiscountOrderSavePlugin—saves order discounts to the
OrderShipmentSavePlugin—saves order shipment information to the
SalesPaymentCheckoutDoSaveOrderPlugin—saves order payments to the
Pre-save condition plugin
SalesOrderExpanderPlugin—expands items by quantity and recalculate quote.
A state machine is triggered in the
CheckoutWorkflow class from the Checkout module; the execution starts after precondition and order saver plugins store no errors into
The state machine trigger needs a name in order to be executed. The name is set by
SalesOrderSaverPlugin in the Sales module. Each project has to implement the
SalesConfig::determineProcessForOrderItem() method, which should return the state machine name for the selected order item. That is, Payolution payment would return
For submitting the form