Comparing Pipelines and SGJC Controllers

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 and Pipelet to Script Method Conversion.

Pipelines to Controllers

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.

Subpipelines to Exported Methods

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 and Controller URLs and SEO

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 to B2C Commerce Script Methods

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
        };
    }

For information on script methods that can be used in place of pipelets, see Pipelet to Script Method Conversion.

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.

Cartridge Folder Structure

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.

Note: If your subpipeline isn't named in accordance with JavaScript method naming conventions, you must rename it to selectively override it. For example, if your subpipeline start node is named 1start, you must rename it before overriding it with a controller method, because the controller method can't have the same name and be a valid JavaScript method.
<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.

Pipeline Dictionary to Global Variables

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.

Calling Scripts and Passing Arguments

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.

Using Pipelines and Controllers Together

Salesforce recommends:
  • Not mixing pipelines and controllers in a single cartridge.
  • Only calling pipelines if they are outside the current cartridge or required for integration of a separate cartridge.
This is recommended because it provides an easier migration path away from pipelines and easier adoption of future features. It also reduces the risk of circular dependencies.

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

A controller can call a pipeline directly and pass it arguments. The call to the pipeline works like a normal subroutine invocation; the controller execution is continued when it returns.
Note: The exception to this is if a pipeline with an interaction-continue-node is called. After rendering the template in the interaction branch of the pipeline, the call returns and the calling controller function immediately continues. A follow-up HTTP request for the continue branch directly invokes the pipeline processor and not go through the controller engine anymore. This means, such a pipeline might not attempt to return to its initial caller (for example, our controller function) with an end node in its continue-branch, because this return has already happened and there is no mechanism to keep JavaScript scopes across multiple requests (in contrast to pipelines, where the whole pipeline dictionary including the call stack gets serialized and stored at the session).

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 key EndNodeName 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.

Public and Private Call Mode to Guards

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

Transactions are defined in pipelines implicitly and explicitly through pipelet attributes. For controllers, transactions are defined through methods.

Implicit Transactions

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.

Explicit Transactions

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.

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.

Forms

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:
profileForm = session.forms.profile;

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.Form.Group.

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:
app.getForm('profile.customer').copyFrom(customer.profile);

.copyTo

dw.web.FormGroup

Updates a system object with information from a form.

This is a replacement for the UpdateObjectWithForm pipelet.

For example:

app.getForm('billing.billingAddress.addressFields').copyTo(billingAddress);

For code examples of forms used with controllers, see Forms.

Back to top.

Interaction Continue nodes and preserving values across requests

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.

FormExample.js includes two functions:
  • 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.

Interaction End Nodes and Unbuffered Responses

Responses from a B2C Commerce application server are buffered by default: when rendering an ISML template the resulting output is written into a buffer first. After the template is rendered without errors, the buffer is written to the HTTP response and sent to the client. In contrast, when no buffering is used, the output from the template will be written immediately to the HTTP response, so the client is receiving it as it's rendered.

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:

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

Whether a response was sent in buffered or streaming mode can be seen from the response HTTP headers:
  • buffered response: contains a Content-Length response header
  • streamed response: contains a Transfer-Encoding response header

Effects 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:

Back to top.

Error Pipelines to Error Controllers

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.

OnRequest and OnSession Event Handler Pipelines to Hooks

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.

Response Rendering

Rendering ISML Templates

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 add function clears the profile form and uses the 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.
In this example, the 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.
Note: As of 16.3, the renderTemplate method now automatically passes all of the pdict variables used by templates, such as CurrentSession and CurrentForms. Previous to 16.3, these had to be passed explicitly in the view.
   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 parameters

In 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.

Rendering JSON

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.

Rendering Result Pages via Redirects

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.

Direct Responses via the Response Object

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.

Note: Anything written into this stream by a controller is not sent immediately to the client, but only after the controller function has returned and no error has occurred.

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.

Caching

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.

Jobs and Third-Party Integrations

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

If you are using a custom or partner cartridge to integrate additional functionality, you have two choices in migrating your integration:
  • convert the cartridge to use controllers.
  • integrate the pipeline cartridge with your storefront. For more information, see the LINK JavaScript Controller FAQ.

Back to top.