Response: From the Backend to the Browser

A plugin method does not print output. It fills a Response object. The framework then sends the object to the browser.

This page tells you:

  • how the Response object gets to the browser
  • how Jimbo.response() in jimbo.js processes it
  • which actions the Response class has
  • how to add your own action

Related pages:

The two properties of a response

A Response has a type and an action.

Property Question that it answers Set with
Type In which format does the server send the data? the constructor, setType()
Action What must the browser do with the data? the constructor, setAction()

All other data are parameters. Set a parameter as a property or with addParam().

$response = new Response(Response::JSON, Response::ACTION_ALERT);

$response->title = __('Done');              // a parameter
$response->addParam('idOrder', 15);         // a parameter
$response->addMessage(__('The order is saved.'));

$response->send();

The server sends the action as the key type of the data, after the parameters. The HTTP status of setStatus() and the response type are not keys of the data. The example above gives this JSON:

{"messages": ["The order is saved."], "title": "Done", "idOrder": 15, "type": "alert"}

How the response gets to the browser

sequenceDiagram participant B as Browser (jimbo.js) participant J as System plugin (Jimbo) participant P as Plugin method participant R as Response B->>J: HTTP request J->>R: new Response(type from the method prefix) J->>P: onJsonSave($response) P->>R: setAction(), parameters, addMessage() P-->>J: return true J->>R: send() Note over R: Core::EVENT_ON_RESPONSE R-->>B: Data in the format of the type B->>B: Jimbo.response(data) Note over B: Calls the function of the action
  1. The system plugin finds the plugin method for the URL.
  2. The system plugin makes the Response. The prefix of the method name sets the type.
  3. The plugin method sets the action and the parameters.
  4. The system plugin calls send(). Do not call send() in a method that the system plugin calls.
  5. send() dispatches Core::EVENT_ON_RESPONSE. Then it prints the data in the format of the type.
  6. In the browser, Jimbo.response(data) reads the key type and does the action.

Method prefix and type

The Jimbo system plugin uses these prefixes:

Prefix Type Use it for
onDisplay Response::NORMAL A page.
onUpdate Response::NORMAL A request that changes data and then shows a page or redirects.
onAjax, doAjaxResponse Response::JSON_IFRAME A form that the browser submits into a hidden <iframe>.
onJson, doJsonResponse Response::JSON A request that JavaScript sends (jQuery.ajax, fetch).

The plugin method gets the Response as its first argument:

public function onJsonSave(Response &$response): bool
{
    $this->object->changeStatus($_POST['id'], 'done');

    $response->addNotification(__('The task is done.'));
    $response->setAction(Response::ACTION_RELOAD);

    return true;
}

Types

Type What the server sends How Jimbo.response() gets the data
Response::NORMAL The HTML page: the content in the main template of the theme. It does not get them. The browser shows the page.
Response::JSON JSON with the header Content-type: application/json. Your JavaScript calls Jimbo.response(data). Jimbo.loadJson() and the DGS do this for you.
Response::JSON_IFRAME <script>parent.Jimbo.response({...});</script> The script runs in the hidden <iframe> and calls Jimbo.response() of the page.
Response::JSON_JS <script>Jimbo.response({...});</script> The script runs in the page that loads the content.
Response::JSON_P <callback>({...}) with the header Content-Type: text/javascript The function that the request names gets the data. See JSONP.
Response::RAW The content with your own Content-Type. It does not get them. See Raw response.
Response::NEW_TAB A script that opens url in a new tab and reloads the page. It does not get them.

Forms and the hidden <iframe>: the DGS forms and the HUI form have the attribute target with the name of a hidden <iframe>. The browser submits the form into that <iframe>, thus the page does not reload and the form can contain files. The reply loads in the <iframe> and calls parent.Jimbo.response(). This is the type Response::JSON_IFRAME. Thus use an onAjax method as the action of such a form.

If a response has no action, the data have no key type. Then Jimbo.response() calls no action function. Your JavaScript reads the data. If the parameters are a list, the reply is a JSON array. If the response has no parameters, the reply is [].

public function onJsonItems(Response &$response): bool
{
    $response->data = $this->object->search();

    return true;
}
{"data": [{"id": 1}]}

What Jimbo.response() does

Jimbo.response(data) does these steps in this sequence:

  1. It hides the loading indicator.
  2. If data.eval has a value, it runs the value as JavaScript.
  3. If data.notifications has a value, it shows each item as a small notification.
  4. If data.type has a value, it calls the function of the action (see below).
  5. If data.js has a value, it runs the value as JavaScript.
  6. It returns true if data.type has a value.

For step 4, Jimbo.response() makes a function name from the action: set + the action name with a capital first letter + Response. An underscore starts a new word.

Action in type Function
alert Jimbo.setAlertResponse(data)
formError Jimbo.setFormErrorResponse(data)
order_saved Jimbo.setOrderSavedResponse(data)

If Jimbo has this function, Jimbo.response() calls it with the full data. If Jimbo does not have it, nothing occurs and no error shows.

Actions

Constant Value What the browser does
Response::ACTION_ALERT alert Shows a message box. Then, optionally, opens a URL or reloads the page.
Response::ACTION_NOTIFICATION notifications Shows each message as a small notification.
Response::ACTION_REDIRECT redirect Opens url.
Response::ACTION_RELOAD reload Loads the current page again.
Response::ACTION_FORM_ERROR formError Shows error messages at form fields.
Response::ACTION_CALLBACK callback Calls the JavaScript function that name names.
Response::ACTION_CONTENT content Adds the HTML in content to the end of <body>.
Response::ACTION_DIALOG dialog Adds the HTML in content to <body> and opens it as a jQuery UI dialog.
Response::ACTION_FILE file The server sends a file for download. See Send a file.
Response::ACTION_LAMBDA lambda The server calls a PHP function that writes the output. See Lambda.

ACTION_FILE and ACTION_LAMBDA are server actions. jimbo.js has no part in them.

ACTION_ALERT

$response->setAction(Response::ACTION_ALERT);
$response->title = __('Saved');
$response->addMessage(__('The order is saved.'));
Parameter Use
messages The lines of the message. Add them with addMessage().
title The title of the box.
url Optional. The browser opens this URL after sleep milliseconds.
reload Optional. true loads the page again after sleep milliseconds.
sleep Optional. Default: 3000.

addMessage() changes HTML characters to entities (htmlentities). Thus a message cannot contain HTML tags.

ACTION_NOTIFICATION and addNotification()

There are two methods to show a small notification.

// 1. An action. The messages come from addMessage().
$response->setAction(Response::ACTION_NOTIFICATION);
$response->title = __('Import');
$response->addMessage(__('12 rows are ready.'));

// 2. A parameter. It works together with each other action.
$response->addNotification(__('The task is done.'));
$response->setAction(Response::ACTION_RELOAD);

With ACTION_REDIRECT or ACTION_RELOAD, the framework keeps the notifications in the session. The browser shows them on the next page. This is the usual method to tell the user that a change is saved.

ACTION_REDIRECT and ACTION_RELOAD

$response->setAction(Response::ACTION_REDIRECT);
$response->url = $this->getUrl('/orders/');

In a method with the type Response::NORMAL (onDisplay, onUpdate), ACTION_REDIRECT sends an HTTP redirect. In the JSON types, jimbo.js changes the location. jimbo.js opens only a path of the website, or an http: or https: URL.

ACTION_FORM_ERROR

There are two formats. You can use the two in one response.

$response->setAction(Response::ACTION_FORM_ERROR);

// 1. By CSS selector
$response->fields = [
    '#email' => __('Enter a correct e-mail address.'),
];

// 2. By the name attribute of the input, in a form
$response->selector = '#profile-form';   // default: form.f-db-form
$response->errors = [
    'email'   => __('Enter a correct e-mail address.'),
    'general' => [__('The profile is not saved.')],
];
Parameter Use
fields An array: CSS selector to message. jimbo.js shows a tooltip at each element.
errors An array: input name to message. jimbo.js looks for the inputs in the form of selector.
errors['general'] A list of messages without a field. jimbo.js shows them as notifications.
errors['inner'] An array: CSS selector to a list of messages. jimbo.js writes them into the element.
selector The CSS selector of the form for errors.

If jimbo.js does not find the element, it shows the message as a notification.

In a plugin method, throw a FieldException. The system plugin makes the ACTION_FORM_ERROR response with fields:

throw new FieldException(__('Enter a correct e-mail address.'), '#email');

For each item of fields, jimbo.js fires Jimbo.EVENT_CALL_ERROR_FIELD. A listener can show the error in a different manner. See Jimbo.js.

ACTION_CALLBACK

Use this action to call a JavaScript function of your plugin with data from the server.

$response->setAction(Response::ACTION_CALLBACK);
$response->name = 'Orders.onSaved';
$response->idOrder = 15;
window.Orders = {
    onSaved: function (data) {
        jQuery('#order-' + data.idOrder).addClass('m-saved');
    }
};

name is a function name, or a path with dots to a function of an object. jimbo.js reads the path from window and calls the function with the full data. It does not run name as JavaScript.

The function, or the first object of the path, must be a property of window when the response arrives. A function declaration and a var at the top level of a script are properties of window. A const, a let, and a class are not, thus the example writes window.Orders. If window does not have the function, jimbo.js throws an error.

Do not put a value from the request into name. Use a fixed function name.

ACTION_CONTENT and ACTION_DIALOG

ACTION_CONTENT adds HTML to the end of <body>. Use it to open a HUI dialog. The HUI dialog contains the script that opens it.

public function onJsonConfirm(Response &$response): bool
{
    $response->setAction(Response::ACTION_CONTENT);
    $response->content = $this->ui->dialog()
        ->setTitle(__('Close the order'))
        ->setBody($this->fetch('close_order.phtml'))
        ->setUrl($this->getUrl('/orders/close/'))
        ->fetch();

    return true;
}

ACTION_DIALOG also adds the HTML, and then it opens the HTML as a jQuery UI dialog (Jimbo.ui.dialog.show()). It reads the title from .e-caption h2 and makes the buttons from .e-buttons button. Use it only for HTML with this structure.

Do not use ACTION_DIALOG for a HUI dialog. The HUI dialog is a Bootstrap modal. ACTION_DIALOG puts a second dialog around it.

Run JavaScript

A response can contain JavaScript in the parameter js. jimbo.js runs it after the action.

$response->js = 'Orders.refreshTotals();';

Use ACTION_CALLBACK when the script needs data. Do not make JavaScript text from user input.

Add your own action

An action is a string. Thus you can add an action without a change to the framework.

1. Backend. Set your action name and the parameters:

public function onJsonHighlight(Response &$response): bool
{
    $response->setAction('ordersHighlight');
    $response->addParam('selector', '#row-5');
    $response->addParam('color', 'red');

    return true;
}

The server sends:

{"selector": "#row-5", "color": "red", "type": "ordersHighlight"}

2. Frontend. Add the function Jimbo.set<Action>Response in a JavaScript file of your plugin:

Jimbo.setOrdersHighlightResponse = function (data) {
    jQuery(data.selector).css('background-color', data.color);
};

3. Register the JavaScript file on the page that sends the request. See Static Files.

Rules:

  • Start the action name with the name of your plugin. Then two plugins cannot use the same name.
  • Do not use the names of the built-in actions.
  • Define the function before the first response arrives. A file that includeStatic() registers loads after jimbo.js, thus this is correct.
  • Declare a constant for the action name in your plugin (const ACTION_HIGHLIGHT = 'ordersHighlight';).

Content without the layout: flush()

A method with the type Response::NORMAL puts its content into the main template of the theme. Call flush() to send only the content.

public function onDisplayRow(Response &$response, int $id): bool
{
    $response->content = $this->fetch('row.phtml', ['row' => $this->object->get($id)]);
    $response->flush();

    return true;
}

Use flush() for an HTML part that JavaScript loads into the page.

To use a different main template, set mainTemplate:

$response->mainTemplate = 'print.phtml';

JSONP

Response::JSON_P sends a function call with the data as the argument. A page from a different domain can load it with a <script> tag.

public function onJsonWidget(Response &$response): bool
{
    $response->setType(Response::JSON_P);
    $response->data = $this->object->getWidgetData();

    return true;
}

The request parameter callback (Response::ACTION_CALLBACK) gives the function name:

GET /orders/widget/?callback=showOrders
showOrders({"data": [1, 2]})

The headers are Content-Type: text/javascript; charset=utf-8 and X-Content-Type-Options: nosniff.

The name must be a JavaScript identifier, or identifiers with a dot between them. An identifier starts with a letter, _ or $, and continues with letters, digits, _ and $. The name has a maximum of 128 characters (JsonpCallbackName::MAX_LENGTH).

callback Result
Not in the request Jimbo.response(...), the function that processes each other reply.
onLoad, app.onLoad, jQuery19_1700000000, $.handlers._done The server calls the function with this name.
Empty, alert(1);//, handlers[0], on load, 1st, a..b, an array (callback[]=x), a name of 129 characters A SystemException. The framework sends no header and no output.
  • The framework does not replace a refused name with the default name. The message of the exception does not contain the refused value.
  • In the JSON, the characters <, >, &, ' and " of a string value are written as \u003C, \u003E, \u0026, \u0027 and \u0022. JSON.parse() and the browser give the same value that the server put in the response.
  • A response that cannot be encoded as JSON (for example, a string that is not UTF-8) gives a SystemException.
  • The classes are Festi\Core\Response\JsonpCallbackName and Festi\Core\Response\JsonpBody. Use them when you make such a reply without a Response.

Caution: Each page of each domain can load a JSONP reply with a <script> tag. Use JSONP only for public data.

HTTP status

setStatus() sets the HTTP status of the types Response::JSON, Response::JSON_P, and Response::RAW. Use the constants of the Response class:

$response->setStatus(Response::STATUS_UNPROCESSABLE_ENTITY);
Constant Status
Response::STATUS_OK 200
Response::STATUS_BAD_REQUEST 400
Response::STATUS_NOT_FOUND 404
Response::STATUS_UNPROCESSABLE_ENTITY 422
Response::STATUS_INTERNAL_SERVER_ERROR 500

Exceptions

When a plugin method throws an exception, the Jimbo system plugin makes the response. The prefix of the method name sets the format.

Method prefix Response
onJson, doJson Response::JSON with ACTION_ALERT
onAjax, doAjax Response::JSON_IFRAME with ACTION_ALERT
other The exception goes to the exception handler of the project.
public function onJsonRemove(Response &$response): bool
{
    if (empty($_POST['id'])) {
        throw new SystemException(__('Select an item.'));
    }
    // ...
}
{"messages": ["Select an item."], "title": false, "type": "alert"}
  • A SystemException shows its display message. Each other exception shows a general message. The user does not see internal details.
  • A FieldException gives ACTION_FORM_ERROR (see above).
  • The third argument of SystemException is an ExceptionContext. It contains a label (the title of the box) or data for the response.
use Festi\Core\Exception\ExceptionContext;

throw new SystemException(
    __('Select an item.'),
    0,
    new ExceptionContext(__('Batch remove'))
);

Before the system plugin sends this response, it dispatches ResponseErrorEvent::EVENT_ON_RESPONSE_ERROR. A listener can change the response.

Send a file

Set the action Response::ACTION_FILE and the path.

public function onDisplayDownload(Response &$response, string $hash): bool
{
    $filePath = $this->object->getFilePath($hash);
    if (!file_exists($filePath)) {
        throw new SystemException("Not found file: ".$filePath);
    }

    $response->setAction(Response::ACTION_FILE);
    $response->path = $filePath;
    $response->fileName = 'report.pdf';           // optional, default: the file name of the path
    $response->contentType = 'application/pdf';   // optional, default: application/octet-stream

    return true;
}

Lambda

Use Response::ACTION_LAMBDA when a function must write the output, for example a large CSV file as a stream.

$response->setAction(Response::ACTION_LAMBDA);

$response->lambda = function () use ($fileName, $columns, $rows) {
    header('Content-Type: text/csv; charset=utf-8');
    header('Content-Disposition: attachment; filename='.$fileName);

    $stream = fopen('php://output', 'w');

    fputcsv($stream, $columns);

    foreach ($rows as $row) {
        fputcsv($stream, $row);
    }

    fclose($stream);
    exit();
};

Raw response (custom Content-Type)

Use Response::RAW to send a template with your own Content-Type, for example an XML sitemap. The server sends the content without the theme.

public function onDisplaySitemap(Response &$response): bool
{
    $response->setType(Response::RAW);
    $response->contentType = 'application/xml; charset=utf-8';
    $response->content = $this->fetch('sitemap.phtml');

    return true;
}
Parameter Use
contentType The value of the Content-Type header.
content The body.
fileName Optional. The server then sends the content as a download with this name.

Use Response without the system plugin

In code that the system plugin does not call (for example an entry script), make the object and call send():

$response = new Response(Response::JSON, Response::ACTION_ALERT);
$response->addMessage(__('The session is closed.'));
$response->send();

Rules

  • Do not use echo, print, header(), or exit() in a plugin method. Fill the Response. The only exception is the function of ACTION_LAMBDA.
  • Use the constants of the Response class for types, built-in actions, and status values.
  • Use addNotification() with ACTION_RELOAD or ACTION_REDIRECT to confirm a change.
  • Use __() for each text that the user sees.