Controllers have many advantages over pipelines.
If you are migrating a storefront based on a pipeline version of SiteGenesis, see Migrating Your Storefront to Controllers for specific steps to take when migrating. If you want to see what the controller equivalent of pipeline nodes are, see: Pipeline to Controller Conversion.
Pipelines are XML files that can be visualized in UX Studio as workflows
Controllers are server-side scripts that handle storefront requests. Controllers orchestrate your storefront’s backend processing, managing the flow of control in your application, and create instances of models and views to process each storefront request and generate an appropriate response. For example, clicking a category menu item or entering a search term triggers a controller that renders a page.
Controllers are written in JavaScript and B2C Commerce script. The file extension of a controller can be either .ds or .js. Controllers must be located in the controllers folder at the top level of the cartridge. Exported methods for controllers must be explicitly made public to be available to handle storefront requests.
Controllers are mapped to URLs, which have the same format as pipeline URLs, with exported functions treated like subpipelines.
Pipeline-Subpipelines, such as Home-Show, are migrated to CommonJS require module exported functions. See also the CommonJS documentation on modules: http://wiki.commonjs.org/wiki/Modules/1.1.
Example 1: Home-Show
The following example is a simple controller that replaces the Home-Show pipeline. The Home.js controller defines a show function that is exported as the Show function.
var app = require('~/cartridge/scripts/app');
var guard = require('~/cartridge/scripts/guard');
/**
* Renders the home page.
*/
function show() {
var rootFolder = require('dw/content/ContentMgr').getSiteLibrary().root;
require('~/cartridge/scripts/meta').update(rootFolder);
app.getView().render('content/home/homepage');
}
exports.Show = guard.ensure(['get'], show);
/** @see module:controllers/Home~includeHeader */
Pipeline URLs |
Pipeline-Subpipeline
|
For example: Home-Show |
Controller URLs |
Module-ExportedFunction
|
For example: Home-Show *(identical to pipeline URLS) |
Because the URLs are identical in format, SEO features work the same whether you are generating URLs with controllers or pipelines.
Pipelets can be replaced with equivalent script methods in most cases. However, if a pipelet doesn't have an equivalent script method, you can call the pipelet directly.
Example: calling the SearchRedirectURL pipelet
This example calls the SearchRedirectURL and passes in the parameters it requires as a JSON object. It also uses the status returned by the pipelet to return an error status.
var Pipelet = require('dw/system/Pipelet');
var params = request.httpParameterMap;
var SearchRedirectURLResult = new dw.system.Pipelet('SearchRedirectURL').execute({
SearchPhrase: params.q.value
});
if (SearchRedirectURLResult.result === PIPELET_NEXT) {
return {
error: false
};
}
if (SearchRedirectURLResult.result === PIPELET_ERROR) {
return {
error: true
};
}
Cartridge path lookup of controllers and Pipelines
When a request arrives for a specific URL, B2C Commerce searches the cartridge path for a matching controller. If the controller is found, it's used to handle the request; otherwise the cartridge path is searched again for a matching pipeline. If the pipeline is found, it's used to handle the request. If a cartridge contains a controller and a pipeline with a matching name, the controller takes precedence.
When searching the cartridge path, B2C Commerce does not verify whether the controller contains the called function in the requesting URL. Calling a controller function that doesn't exist causes an error.
Back to top.
Cartridges can contain either controllers and pipelines together or separately. Controllers must be located in a controllers folder in the cartridge, at the same level as the pipelines folder. If you have controllers and pipelines that have the same name in the same cartridge, B2C Commerce uses the controller and not the pipeline.
<cartridge>
+-- modules
+-- package.json
+-- cartridge
+-- controllers (the new JavaScript controllers)
+-- forms
+-- pipelines
+-- scripts
+-- hooks.json
+-- models
+-- views
+-- static
+-- templates
+-- webreferences
+-- webreferences2
+-- package.json
The package.json
outside the cartridge
structure can be used to manage build dependencies. The hooks.json file
inside the cartridge structure is used to manage hooks and the web service
registry.
Controllers don't have access to the Pipeline Dictionary. Instead, controllers and scripts have access to information through global variables and B2C Commerce script methods. This information is explicitly passed to scripts previously called in script nodes.
B2C Commerce scripts define input parameters. Input is passed into the execute function input parameter of the script (usually pict or args). The execute function is always the top-level function in any B2C Commerce script.
Example 1: the ValidateCartForCheckout.js script
In this example, the script has two input parameters, Basket and ValidateTax. The input is passed into the execute function via the pdict parameter. * @input Basket : dw.order.Basket
* @input ValidateTax : Boolean
*/
function execute (pdict) {
validate(pdict);
return PIPELET_NEXT;
}
Example 2: passing input to a script in pipelines
In pipelines, input variables are defined in the script node that calls the script.
In this example, a script node in the COPlaceOrder pipeline Start subpipeline calls the ValidateCartForCheckout.js script and passes two input parameters into the execute method:
Example 3: passing input into a script in controllers
In controllers, arguments are passed into scripts as JSON objects. Controllers use the require method to call script functions, if the script is converted into a module.
The COPlaceOrder.js controller creates a CartModel that wraps the current basket and calls the validateForCheckout function (exported as ValidateForCheckout). The validateForCheckout function calls the ValidateCartForCheckout.js script and passes in the input parameters as a JSON object.
validateForCheckout: function () {
var ValidateCartForCheckout = require('app_storefront_core/cartridge/scripts/cart/ValidateCartForCheckout');
return ValidateCartForCheckout.validate({
Basket: this.object,
ValidateTax: false
});
},
Back to top.
Controller overlay Cartridges
In general, Salesforce recommends creating a separate cartridge for controllers to replace existing pipelines when migrating your site. Controller cartridges are always used in preference to cartridges that use pipelines, no matter where they are in the cartridge path. This approach lets you incrementally build functionality in your controller cartridge and to fall back to the pipeline cartridge if you experience problems, by removing the controller from the cartridge or the controller cartridge from the cartridge path.
Pipelines and Controllers in the same Cartridge
Pipelines and controllers can be included in the same cartridge, if they are located in the correct directories.
If the
pipeline-subpipeline
names do not collide with those of
the controller-function
names, they work in parallel. If
they share the same name, the controller is used.
A storefront can have some features that use pipelines, while others use controllers. It's also possible to serve remote includes for the same page with controllers or pipelines, because they are independent requests with separate URLs.
Controllers calling Pipelines
Example: Call MyPipeline-Start
This example calls the MyPipeline pipeline Start subpipeline and passes it three arguments.
let Pipeline = require('dw/system/Pipeline');
let pdict = Pipeline.execute('MyPipeline-Start', {
MyArgString: 'someStringValue',
MyArgNumber: 12345,
MyArgBoolean: true
});
let result = pdict.MyReturnValue;
Determining the end node of a pipeline
The dw.system.Pipeline class execute method returns the name of the end node at which the pipeline finished. If the pipeline ended at an end node, its name is provided under the keyEndNodeName
in the returned pipeline dictionary result.
If the dictionary already contains an entry with such a key, it is
preserved. Pipelines calling Controllers
Pipelines can't call controllers using call or jump nodes. The pipeline must contain a script node with a script that can use the require function to load a controller.
Pipelines can call controllers using script nodes that require the controller as a module. However, this isn't recommended. The controller functions that represent URL handlers are typically protected by a guard (see Public and Private Call Nodes to Guards). Such functions shouldn't be called from a pipeline. Only local functions that don't represent URLs should be called. Ideally, such functions would not be contained in the controllers folder at all, but moved into separate modules in the scripts directory. These scripts are not truly controllers, but regular JavaScript helper modules that can be used by both controllers and pipelines.Example: Call MyController
/*
* @output Result: Object
*/
importPackage(dw.system);
function execute(pdict: PipelineDictionary): Number
{
let MyController = require('~/cartridge/controllers/MyController');
let result = MyController.myFunction();
pdict.Result = result;
return PIPELET_NEXT;
}
Back to top.
In some cases you need to control access to a unit of functionality. For pipelines, this means securing access to the pipeline Start node. For controllers, it means securing access to the exported functions of the controller.
For pipelines, pipeline Start nodes let you set the Call Mode property to:
For controllers, a function is only called if it has a
public
property that is set to true
. All
other functions that don't have this property are ignored by B2C Commerce and
lead to a security exception if an attempt is made to call them using HTTP
or any other external protocol.
Controllers use functions in the guard.js module to control access to functionality by protocol, HTTP methods, authentication status, and other factors.
Additional information about guards is available in secure request access.
Back to top.
Transactions are defined in pipelines implicitly and explicitly through pipelet attributes. For controllers, transactions are defined through methods.
For pipelines, some pipelets let you create implicit transactions, based on whether the pipelet has a Transactional property that can be set to true.
For controllers, use the B2C Commerce System package Transaction class wrap method to replace implicit transactions.
Example: wrap an implicit transaction
var Transaction = require('dw/system/Transaction');
Transaction.wrap(function() {
couponStatus = cart.addCoupon(couponCode);
});
Back to top.
For pipelines, you define transaction boundaries using the Transaction Control property on a series of nodes to one of four values: Begin Transaction, Commit Transaction, Rollback Transaction, or Transaction Save Point.
For controllers, use the B2C Commerce System package Transaction methods to create and manage explicit transactions.
Transaction.begin
, but before
Transaction.commit
is called (for example, if
sending an email fails), the transaction is usually rolled back,
unless the error is a pure JavaScript error that doesn't involve
any B2C Commerce APIs.Transaction.rollback
method is
explicitly called, the rollback happens at that point.Example: create an explicit transaction with a rollback point and additional code after the rollback
var Transaction = require('dw/system/Transaction');
Transaction.begin();
if {
code for the transaction
…
}
else {
Transaction.rollback();
code after the rollback
…
}
}
Transaction.commit();
Back to top.
Controllers can use the existing form framework for handling requests for web forms. For accessing the forms, the triggered form, and the triggered form action, use the dw.system.session and dw.system.request B2C Commerce script methods as an alternative to the Pipeline Dictionary objects CurrentForms, TriggeredForm and TriggeredAction.
Expression |
Type |
Description |
---|---|---|
session.forms |
dw.web.Forms |
The container for forms. This is a replacement for the CurrentForms variable in the Pipeline Dictionary. For example:
|
request.triggeredForm |
dw.web.Form |
The form triggered by the submit button in the storefront. This is an alternative for the TriggeredForm variable in the Pipeline Dictionary. |
request.triggeredFormAction |
dw.web.FormAction |
The form action triggered by the submit button in the storefront. This is an alternative for the TriggeredAction variable in the Pipeline Dictionary. |
Updating B2C Commerce system/custom Objects with Form Data
You can copy data to and from B2C Commerce objects using methods in the dw.web.FormGroup.
Expression |
Type |
Description |
---|---|---|
.copyFrom |
dw.web.FormGroup |
Updates the CurrentForm object with information from a system object. This is a replacement for the UpdateFormWithObject pipelet. For example:
|
.copyTo |
dw.web.FormGroup |
Updates a system object with information from a form. This is a replacement for the UpdateObjectWithForm pipelet. For example:
|
copyFrom()
and copyTo()
methods to
copy values from one custom object to another, since copyTo()
works
only with submitted forms. Instead, use Javascript to directly copy the values, as
in this
example:let testObject = { name:"default name", subject:"default subject", message:"default message" };
let output = {};
Object.keys( testObject ).forEach( function( key ) {
output[key] = testObject[key];
});
Back to top.
Because controllers have nothing like the Pipeline Dictionary that is preserved across requests; local variables in forms have to be resolved for each request. However, for templates that use URLUtils.continueURL() for forms, it's possible to pass a ContinueURL property to the template that is used as a target URL for the form actions. In SiteGenesis, the target URL is to a controller that contains a form handler with functions to handle the actions of the form. Usually, this is the same controller that originally rendered the page.
The examples in this section show how login form functionality works in the application. Example 1 shows the controller functionality to render the page and handle form actions. Example 2 shows the template with the form that is rendered and whose actions are handled.
Example 1: Rendering the login_form template and passing the ContinueURL property.
start
- this is the public entrypoint for the
controller and renders the mylogin page, which has a login form.formHandler
- this function is used to handle
form actions triggered in the mylogin page. The function uses the
app.js getForm
function to get a
FormModel
object that wraps the login form and then
uses the FormModel
handleAction
function to determine the function to call to handle the triggered
action. The formHandler method defines the functions to call depending
on the triggered action and passes them to the
handleAction
function.FormExample.js
function start() { ... app.getView({ ContinueURL: URLUtils.https('FormExample-LoginForm') }).render('account/mylogin'); } function formHandler() { var loginForm = app.getForm('login'); var formResult = loginForm.handleAction({ login: function () { response.redirect(URLUtils.https('COShipping-Start')); return; } }, register: function () { response.redirect(URLUtils.https('Account-StartRegister')); return; }, } }); exports.Start = guard.ensure(['https'], start); FormHandler = guard.ensure(['https', 'post'], formHandler);
Example 2: Setting a URL target for the form action in the ISML template.
The template contains two forms with actions that can be triggered.
The
call to URLUtils.httpsContinue()
resolves to the value
for the ContinueURL
property set in the previous example,
which is to the form handler function for the
form.
login_form.isml
<form action="${URLUtils.httpsContinue()}" method="post" id="register">
...
</form>
<form action="${URLUtils.httpsContinue()}" method="post" id="login">
...
</form>
Back to top.
The default buffering behavior is the best choice for the average web page. However, if you need to render a large response as a page without affecting performance because of increased memory consumption caused by buffering the page, you can change the response mode to streaming.
In pipelines it's possible to set the buffered attribute
to false
for interaction end nodes. In controllers, use
the dw.system.Response
class
setBuffered(boolean)
method for a response. The default
is still buffered mode.
Reasons to enable or disable Buffering
Buffering is enabled by default, and this is the right choice for most situations. Using a response buffer is good for error handling, because in case of problems the whole buffer can simply be skipped and another response can be rendered instead, for example an error page. In unbuffered streaming mode, this would not work, because parts of the response might already have been sent to the client.
For very big responses, the response buffer might become very large and consume lots of memory. In such rare cases it's better to switch off buffering. With streaming mode, the output is sent immediately, which doesn't consume any extra memory. So use streaming if you must generate very large responses.
Methods to enable or disable Buffering
There are two ways to enable or disable buffering:
false
or true
.
The property view of the UX Studio pipeline editor must be set to
"Show Advanced Properties" for you to see the property.Example: "Hello-World" controller that generates a non-buffered response:
exports.World = function(){ response.setBuffered(false); response.setContentType('text/plain'); var out = response.writer; for (var i = 0; i < 1000; i++) { out.println('Hello World!'); } }; exports.World.public = true;
Detecting Buffering or Streaming
Content-Length
response headerTransfer-Encoding
response headerEffects of buffering on the page Cache
Buffered responses can be cached in the page cache, if they specify an expiration time and page caching is enabled for the site. Streamed responses are never cached.
Buffering and remote Includes
Buffered responses can have remote includes. If a page has remote includes, the remote includes are resolved in sequence and then the complete response is assembled from the pieces and returned to the client. Because they must be resolved and assembled before returning a response, remote includes can't be streamed and must always be buffered.
A streamed response can't have remote includes, as would not be resolved. Streaming can only be used for top-level requests without any remote includes.
Troubleshooting Buffering
There are some situations when the response is sent buffered even if buffering has actually been disabled:
The response is too small
If less than 8000 characters are sent, the response will still be buffered.
The Storefront Toolkit is active
If the Storefront Toolkit is enabled (like on development sandboxes), it post-processes the responses from the application server. This includes parsing them and inserting additional markup that is needed for the various views of the Storefront Toolkit in the browser. This process deactivates any buffering.
Back to top.
The Error-Start pipeline is called when the originating pipeline doesn't handle an error. The Error controller has the reserved name of Error.js. It's called whenever another controller or pipeline throws an unhandled exception or when request processing results in an unhandled exception.
Similar to the Error pipeline, an Error controller has two entry points:
The error functions get a JavaScript object as an argument that contains information about the error:
Back to top.
The onrequest and onsession pipelines can be replaced with onrequest and onsession hooks. The hook name and extension point are defined in the hooks.json file.
These hooks reference script modules provided in SiteGenesis, in the app_storefront_controllers cartridge, in the /scripts/request folder.
"hooks": [
{
"name": "dw.system.request.onSession",
"script": "./request/OnSession"
},
{
"name": "dw.system.request.onRequest",
"script": "./request/OnRequest"
})
…
Back to top.
Controllers use the ISML class renderTemplate method
to render template and pass any required parameters to the template. The
argument is accessible in the template as the pdict variable and
its properties can be accessed using pdict.*
script
expressions. However, this doesn't actually contain a Pipeline Dictionary,
as one doesn't exist for controllers. However, passing the argument
explicitly lets existing templates be reused.
Example 1: rendering a template in a controller
This example shows the simplest method of rendering a template in a controller. Usually, a view is used to render a template, because the view adds all the information normally needed by the template. However, this example is included for the sake of simplicity.
Hello.js
let ISML = require('dw/template/ISML');
function sayHello() {
ISML.renderTemplate('helloworld', {
Message: 'Hello World!'
});
}
Example 2: using the pdict variable in ISML templates
The ${pdict.Message}
variable resolves to
the string "Hello World" that was passed to it via the
renderTemplate
method in the previous
example.
helloworld.isml
<h1>
<isprint value="${pdict.Message}" />
</h1>
SiteGenesis uses View.js and specialized view scripts, such as CartView.js to get all of the parameters normally included in the Pipeline Dictionary and render an ISML template.
Example 1: controller creates the view.
In this example, the Address
controller
function clears the
profile form and uses the add
app.js
getView
function to get a view that renders the addressdetails
template and passes the Action
and
ContinueURL
parameters to the template. The
getView
function creates a new instance of the
View
object exported by the View.js
module. The parameters passed to the getView
function are
added to the View object when it's initialized.
The controller then
calls the render
method of the View.js
module to render the addressdetails.isml
template.
/**
* Clears the profile form and renders the addressdetails template.
*/
function add() {
app.getForm('profile').clear();
app.getView({
Action: 'add',
ContinueURL: URLUtils.https('Address-Form')
}).render('account/addressbook/addressdetails');
}
Example 2: view renders the template.View.js
view script assembles information for template,
renders the template, and passes it the information. The View.js script is
the main module used to render templates. Other view modules that render
specific templates, such as CartView.js extend the View object exported by
View.js. render: function (templateName) {
templateName = templateName || this.template;
// provide reference to View itself
this.View = this;
try {
ISML.renderTemplate(templateName, this);
} catch (e) {
dw.system.Logger.error('Error while rendering template ' + templateName);
throw e;
}
return this;
}});
Example 3: template uses the passed parametersIn this
example, there are two lines from the addressdetails.isml
template, in which the template uses the Action
parameter
passed from the Address controller and the CurrentForms
parameter passed from the View.js
renderTemplate
method as $pdict
variables.
...
<isif condition="${pdict.Action == 'add'}">
...
<input type="hidden" name="${pdict.CurrentForms.profile.secureKeyHtmlName}" value="${pdict.CurrentForms.profile.secureKeyValue}"/>
The view renders the passed template and adds any passed parameters to the global variable and request parameters passed to the template.
Back to top.
SiteGenesis provides a function to render JSON objects in the Response.js module.
Example 1: rendering a JSON object
function sayHelloJSON() {
let r = require('~/cartridge/scripts/util/Response');
r.renderJSON({
Message: 'Hello World!'
});
}
This returns a response that looks like:
{"Message": "Hello World!"}
Example 2: rendering a more complex JSON object
let r = require('~/cartridge/scripts/util/Response');
r.renderJSON({({
status : couponStatus.code,
message : dw.web.Resource.msgf('cart.' + couponStatus.code, 'checkout', null, couponCode),
success : !couponStatus.error,
baskettotal : cart.object.adjustedMerchandizeTotalGrossPrice.value,
CouponCode : couponCode
});
This method can accept JavaScript objects and object literals.
Back to top.
Controllers that handle forms in POST requests usually end with an HTTP redirect to view a result page instead of directly rendering a response page. This avoids problems with browser back buttons and multiple submissions of forms after refreshing a page. For sending redirects, use the response.redirect() methods .
function sayHello() {
// switches to HTTPS if the call is HTTP
if (!request.isHttpSecure()) {
response.redirect(request.getHttpURL().https());
return;
}
response.renderJSON({
Message: 'Hello World!'
});
}
Back to top.
A controller is able to send responses by directly writing into the output stream of the response object using a Writer method that represents the underlying response buffer.
The response object also enables you to set the content type, HTTP status, character encoding and other relevant information.
Example: direct response
function world() {
response.setContentType('text/html');
response.getWriter().println('<h1>Hello World!</h1>');
}
Back to top.
You can use a different template cache value with the response.setExpires method, both values are evaluated and the lesser of the two values is used. This is similar to how remote includes behave.
Caching behavior is set in the following ways:
If multiple calls to setExpires() or to the <iscache> tag are done within a request, the shortest caching time of all such calls wins.
function helloCache() {
let Calendar = require('dw/util/Calendar');
// relative cache expiration, cache for 30 minutes from now
let c = new Calendar();
c.add(Calendar.Minute, 30);
response.setExpires(c.getTime());
response.setContentType('text/html');
response.getWriter().println('<h1>Hello World!</h1>');
Back to top.
Both jobs and third-party integrations are peripheral to storefront code.
Jobs
Job pipelets don't have script equivalents. For this reason, jobs can't be migrated to controllers. Any job you create must use pipelines.
Third-Party Integrations
Back to top.