Views: 3775
Last Modified: 20.01.2022

Controllers is the part of MVC architecture, responsible for query processing and response generation.

Actions

Controllers consist of main operational actions, employed by user to get a result. Single controller can have one or several actions. For example, let's create controller with two actions item.add and item.view in the module example.

First step - create file .settings.php in the module root.

<?php
//modules/vendor.example/.settings.php
return [
	'controllers' => [
		'value' => [
			'defaultNamespace' => '\\Vendor\\Example\\Controller',
		],
		'readonly' => true,
	]
];

Next, create controller file (see details for available methods here):

namespace Vendor\Example\Controller;

use \Bitrix\Main\Error;

class Item extends \Bitrix\Main\Engine\Controller
{
	public function addAction(array $fields):? array
	{
		$item = Item::add($fields);

		if (!$item)
		{
			$this->addError(new Error('Could not create item.', {error_code}));

			return null;
		}

		return $item->toArray();
	}

	public function viewAction($id):? array
	{
		$item = Item::getById($id);
		if (!$item)
		{
			$this->addError(new Error('Could not find item.', {error_code}));
					
			return null;
		} 

		return $item->toArray();
	}
}

First, the add action (using specific method Item::addAction) attempts to create a specific Item passed by $fields.

Note. Array $fields retrieved by automatic binding of method parameters and $_REQUEST.

When an activity was failed due to specific reasons, return null and complete the controller by errors. In this case, kernel generates a response:

{
	"status": "error", //please note that status has been automatically updated
	"data": null,
	"errors": [
		{
			"message": "Could not create item.",
			"code": {code}
		}
	]
}

Otherwise, add an item and return it as an array $item->toArray() from activity. This array, kernel generates the response:

{
	"status": "success",
	"data": {
		"ID": 1,
		"NAME": "Nobody",
		//...element fields
	},
	"errors": null
}

Generally, activity can return not only scalars, but objects as well.

The view activity (using specific method Item::viewAction) first attempts loading a specific Item object by specific passed parameter $id. It's important to note, the $id will be automatically retrieved from $_POST['id'] or $_GET['id'].

When this parameter is not found, kernel generates response with error:

{
	"status": "error",
	"data": null,
	"errors": [
		{
			"message": "Could not find value for parameter {id}",
			"code": 0
		}
	]
}

How execute controller action?

Specific AJAX-action requires action naming convention awareness. In our case: Item::addAction -> vendor:example.item.add Item::viewAction -> vendor:example.item.view.

vendor:example.item.add, vendor:example.item.view can be used for calling actions using BX.ajax.runAction:

BX.ajax.runAction('vendor:example.item.add', {
	data: {
		fields: {
            ID: 1,
            NAME: "test"
        } 
	}
}).then(function (response) {
	console.log(response);
	/**
	{
		"status": "success", 
		"data": {
			"ID": 1,
			"NAME": "test"
		}, 
		"errors": []
	}
	**/			
}, function (response) {
	//receives all responses with status status !== 'success'
	console.log(response);
	/**
	{
		"status": "error", 
		"errors": [...]
	}
	**/				
});

Or retrieve the link to action and send individual http-query.

/** @var \Bitrix\Main\Web\Uri $uri **/
$uri = \Bitrix\Main\Engine\UrlManager::getInstance()->create('vendor:example.item.view', ['id' => 1]);
echo $uri;
// /bitrix/services/main/ajax.php?action=vendor:example.item.view&id=1
// execute GET-query

Creating controllers and actions

Creating controllers

Controllers must be inherited from \Bitrix\Main\Engine\Controller or its descendants. Controllers can be located inside the module, or inside the component in the file ajax.php and be a controller for component.

Creating actions

Creating actions means creating methods in a specific controller. Method requires to be public and have an Action suffix.

namespace Vendor\Example\Controller;

class Item extends \Bitrix\Main\Engine\Controller
{
	public function addAction(array $fields)
	{
        //...
	}
}

Returns action value is response data that can be sent to client.

If action returns \Bitrix\Main\HttpResponse or its descendants, this object is sent to client. When action returns data, it must be converted to scalar or object format, to be converted to JSON for generating \Bitrix\Main\Engine\Response\AjaxJson.

Cumulatively, the action can return both scalars but objects as well that implement the following interfaces:

  • \JsonSerializable
  • \Bitrix\Main\Type\Contract\Arrayable
  • \Bitrix\Main\Type\Contract\Jsonable

Or specific descendant \Bitrix\Main\HttpResponse:

Creating action-classes

There is an option to create action classes, inherited from \Bitrix\Main\Engine\Action. This option may be required, when logic must be repeatedly used in several controllers. For example, for implementing the same exchange protocol in different modules (e. g. standard search, executing step-by-step actions with progress and etc.). You need to describe the method configureActions in the controller config map:

class Test extends \Bitrix\Main\Engine\Controller
{
	public function configureActions()
	{
		return [
			'testoPresto' => [
				'class' => \TestAction::class,
				'configure' => [
					'who' => 'Me',
				],
			],
		];
	}
}

Here's the TestAction itself:

<?php

use \Bitrix\Main\Engine\Action;

class TestAction extends Action
{
	protected $who;

	//method for additional config from controller. When requires to set
	//some values into internal status
	public function configure($params)
	{
		parent::configure($params);

		$this->who = $params['who']?: 'nobody';
	}

	//main operational method. Parameters are automatically associated the same way as in the method
	//ajax-action
	public function run($objectId = null)
	{
		return "Test action is here! Do you know object with id {$objectId}? Mr. {$this->who}";
	}
}

To call this action, query testoPresto, as described in the config map. Class action supports pre- and post- filters and are no different from the standard action method. run() method reason is similar to other methods/ajax-actions.

Using controllers inside components

Preliminarily create and use controller classes, located in modules, as listed in this article, because this allows to organize better a repeated use of auxiliary code and business logic.

In simple cases, when component is self-sufficient and is not actively used with module's API, you can use controllers inside the component.

Controller life cycle

Application query create controller based on naming convention. Next, controller executes the following work:

  • Controller::init() is called after controller was created and configured.
  • Controller creates an action object
    • When action creating is unsuccessful, throws an exception.
  • Controller requests the parameter preparation method Controller::prepareParams.
  • Controller call the method Controller::processBeforeAction(Action $action); when returns true, the execution continues.
  • Controller throws main module event {full_name_controller_class}::onBeforeAction, when returns EventResult not equal to EventResult::SUCCESS, execution is blocked. Prefilters are executed in this event.
  • Controller triggers the action
    • Action parameters are matched with query data
  • Controller throws the main module event {full_name_controller_class}::onAfterAction. Postfilters are executed in this event.
  • Controller calls the method Controller::processAfterAction(Action $action, $result).
  • Application receives an action result and if result is data, creates \Bitrix\Main\Engine\Response\AjaxJson with this data, or sends response object from action.
  • Application calls the method Controller::finalizeResponse($response), by passing final response variant, which will be sent to user after all events and preparations.
  • Prints $response to user.

Several namespaces

Indicating several namespaces in the module.

You can indicate several namespaces in .settings.php, in addition to defaultNamespace. This can may be necessary, when controllers are located next to their workflow entities. For example, in a "Drive" module we have integration with clouds.

<?php
return [
	'controllers' => [
		'value' => [
			'namespaces' => [
				'\\Bitrix\\Disk\\CloudIntegration\\Controller' => 'cloud', //cloud - is an alias
			],
			'defaultNamespace' => '\\Bitrix\\Disk\\Controller',
		],
		'readonly' => true,
	]
];

Now you have call-available controllers, located in both namespaces. Both of them support calling via action full name and via abbreviated record, due to available cloud alias.

Similar to:

disk.CloudIntegration.Controller.GoogleFile.get
disk.cloud.GoogleFile.get

disk.Controller.File.get
disk.File.get

Calling modular controller

Calling modular controller with signed component parameters

When you need to query the controller, implemented in the module, use the following method from component context by getting signed parameters:

BX.ajax.runAction('socialnetwork.api.user.stresslevel.get', {
    signedParameters: this.signedParameters, // result $this->getComponent()->getSignedParameters() 
    data: {
        from: myComponentName, // for exmple, 'bitrix:intranet.user.profile', which parameters we require
        fields: {
            //..
        }
    }
});

After this, inside this action code, use:

<?php

    //...
    public function getAction(array $fields)
    {
        //inside is unpacked, checked array with parameters
        $parameters = $this->getUnsignedParameters();
        
        return $parameters['level'] * 100;
    }


Courses developed by Bitrix24