bitrix-controllers
Engine Controller/JsonController: thin actions, filter attributes, CurrentUser, errors. Use for AJAX/REST/routed endpoints.
Install
npx skills add https://github.com/bxmaximum/bitrix-framework-skills/tree/main/skills/bitrix-controllers
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install bxmaximum-bitrix-framework-skills@llmmart
git clone https://github.com/bxmaximum/bitrix-framework-skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole bxmaximum/bitrix-framework-skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
Bitrix Controllers
Baseline: main 23.0+. Features newer than baseline are marked Since.
Progressive disclosure: open only the rule files that match the task. Do not read every rules/*.md.
How to use
- Identify the layer the task touches.
- Open the matching
rules/*.mdbelow. - Prefer framework-native Bitrix patterns over custom abstractions.
Choose a rule file
When to read rules/basics.md
Read rules/basics.md (Location, thin controller, autowire) when the task involves:
- Location and Naming
- Minimal Controller
- Action Parameter Autowiring
- Controller Lifecycle
- Additional Autowire Types
- Front-end Call
When to read rules/filters.md
Read rules/filters.md (Filters and attributes) when the task involves:
- Default Prefilters
- Action Filters
- PHP 8 Attribute Filters (preferred)
#[ActionAccess]/AccessCheck(module ACL)
When to read rules/errors-response.md
Read rules/errors-response.md (Errors, responses, scope) when the task involves:
- Errors
- Response Types
- Scope (AJAX / REST / CLI)
- Checklist
Cross-cutting invariants (apply regardless of rule file)
- Never configure the same action both via filter attributes and
configureActions()— the controller fails withInvalid configuration of actions. - Do not register one controller both as an HTTP route target (
/local/routes/web.php) and in the AJAXcontrollers.defaultNamespaceof.settings.php. Keep separateWeb\*andAjax\*controllers, each with its owngetAutoWiredParameters(). - Rendering helpers
renderView()/renderComponent()/renderExtension()— Since main 25.700.0. They return HTML (HttpResponse-based) and are for HTTP routes only;BX.ajax.runAction()expects JSON. For AJAX userenderComponentAjax()— JSON withhtml,assets,additionalParams,componentResult.renderExtension()/renderView()/renderComponent()acceptwithSiteTemplate: falseto skip the site template.renderExtension()requirescontrollerEntrypointin the extension'sconfig.php; it renders in the browser (not SSR). PageNavigationautowire (global, nav idnav) accepts a page size only within 1–50 (setPageSizes(range(1, 50))); an out-of-rangesizeis silently ignored and the default 20 is used.- Request DTOs are wired with
new ValidationParameter(...)ingetAutoWiredParameters()— not a#[ValidationParameter]attribute (that class does not exist). #[ActionAccess]requires the controller to implementAccessCheckControllerInterface. Without it the engine throwsSystemException.
Checklist
- Opened only the rule file(s) needed for this task.
- Followed DI /
/local// security canons fromAGENTS.md. - Respected the cross-cutting invariants above (no attribute +
configureActions()mix, separate Web/Ajax controllers).
Files (bitrix-framework-skills)
-
rules
-
basics.md 5.2 KB
# Location, thin controller, autowire ## Location and Naming - Files: `/local/modules/<vendor>.<module>/lib/Infrastructure/Controller/<Name>.php`. - Namespace (default): `\Vendor\Module\Infrastructure\Controller\<Name>`. - Public URL for AJAX: `/bitrix/services/main/ajax.php?action=vendor:module.<name>.<action>`. - URL can be rewritten by a route (see `bitrix-routing`). Namespace configuration — in `/local/modules/vendor.module/.settings.php`: ```php 'controllers' => [ 'value' => [ 'defaultNamespace' => '\\Vendor\\Module\\Infrastructure\\Controller', 'namespaces' => [ '\\Vendor\\Module\\Infrastructure\\Controller\\Web' => 'web', ], 'restIntegration' => ['enabled' => true], // for REST ], 'readonly' => true, ], ``` Access to `Web\PostController::getAction` → `?action=vendor:module.web.post.get`. ## Minimal Controller `ControllerBuilder` builds the controller with `Request` only (`newInstance($request)`). Do **not** put custom services in the controller constructor — inject them via **action method parameters** (autowire). Avoid manual `new Service()` / ServiceLocator lookups inside actions when autowire works. Keep `*Action()` as **orchestration only**: accept input → call application service → map `Result` / errors → return data or response helper. No fat business logic, heavy transforms, or hidden side effects in the controller. Prefer **PHP 8 attributes** for filters. Use `configureActions()` only for compatibility or cases attributes cannot express. ```php <?php declare(strict_types=1); namespace Vendor\Module\Infrastructure\Controller; use Bitrix\Main\Engine\Controller; use Bitrix\Main\Engine\ActionFilter; use Bitrix\Main\Engine\ActionFilter\Attribute\Rule\Authentication; use Bitrix\Main\Engine\ActionFilter\Attribute\Rule\HttpMethod; use Bitrix\Main\Engine\ActionFilter\Attribute\Rule\DisablePrefilters; use Bitrix\Main\Engine\CurrentUser; use Bitrix\Main\Error; use Vendor\Module\Application\Service\PostService; final class Post extends Controller { #[HttpMethod([HttpMethod::METHOD_GET])] #[DisablePrefilters([ActionFilter\Csrf::class])] public function getAction(int $id, PostService $postService): array { $post = $postService->find($id); if ($post === null) { $this->addError(new Error('Not found', 'POST_NOT_FOUND')); return []; } return ['post' => $post]; } #[Authentication] #[HttpMethod([HttpMethod::METHOD_POST])] public function createAction( string $title, string $body, PostService $postService, CurrentUser $currentUser, ): array { $result = $postService->create($title, $body, (int)$currentUser->getId()); if (!$result->isSuccess()) { $this->addErrors($result->getErrors()); return []; } return $result->getData(); } } ``` ### `JsonController` `Bitrix\Main\Engine\JsonController` extends `Controller` and adds `ContentType([JSON])` to default prefilters. Use it for JSON-body APIs; otherwise prefer plain `Controller`. ## Action Parameter Autowiring Action parameters are collected by the engine in the following order: 1. **Scalar types** (`int`, `string`, `bool`, `float`, `array`) → from `GET`/`POST`/`FILES`. 2. **Service objects** → from `ServiceLocator` by name/type. 3. **`HttpRequest`, `Session`, `CurrentUser`** → from context. 4. **Request DTO** via `Bitrix\Main\Validation\Engine\AutoWire\ValidationParameter` in `getAutoWiredParameters()` → mapping + validation (see `bitrix-validation`). This is an AutoWire `Parameter` subclass, **not** a PHP attribute. 5. **ORM objects**, if the action accepts `EntityObject` — loaded by `id`. Missing mandatory parameter → automatic error. For a cohesive input set (validation, nested structure, pagination), prefer a Request DTO over a long list of scalars. ### Current user - Prefer `CurrentUser $user` in the action signature, or `$this->getCurrentUser()`. - Do **not** use global `$USER` inside new controller code. ## Controller Lifecycle 1. Constructor — engine passes `Request` only (`ControllerBuilder`). No ServiceLocator DI for custom services. 2. `init()` — called from the constructor; load modules if needed (`parent::init()` first when overriding). Prefer action-parameter autowire over resolving services in `init()`. 3. Prefilters run. 4. Action method executes. 5. Postfilters run. 6. Response is serialized. Do not hide action logic in `init()`, `__construct()`, `processBeforeAction()`, or `processAfterAction()`. `executeComponent()` in component controllers does **not** run during AJAX actions — use `onPrepareComponentParams()` for shared setup. ## Additional Autowire Types - `Bitrix\Main\Engine\CurrentUser` — current user context. - `Bitrix\Main\Engine\JsonPayload` — raw JSON body. - `Bitrix\Main\UI\PageNavigation` — pagination from request. Custom DTO autowiring via `getAutoWiredParameters()`. ## Front-end Call ```js BX.ajax.runAction('vendor:module.post.create', { data: { title: 'Title', body: 'Body' }, }).then((response) => { console.log(response.data); }); ``` For REST — `BX.rest.callMethod('vendor.module.post.create', {...})`. -
errors-response.md 3.1 KB
# Errors, responses, scope ## Errors - `$this->addError(new \Bitrix\Main\Error('msg', 'CODE', ['key' => 'value']));` - `$this->addErrors($result->getErrors());` - Never throw exceptions outward for ordinary user errors — use `Result` + `Error` (see `bitrix-result-and-errors`). - Response with errors automatically receives `status: 'error'` and `errors` array. - When returning success data from a service `Result`, prefer a narrow `getData()` contract — do not leak internal structures. ## Response Types - `array` → JSON: `{ "status": "success", "data": [...] }`. - `null` → `{ "status": "success" }` without data. - `Bitrix\Main\HttpResponse` — custom response (headers, status, body). - `Bitrix\Main\Engine\Response\HtmlContent` — AJAX JSON with `html` + `assets` (extends `AjaxJson`). - `Bitrix\Main\Engine\Response\Json` / `Redirect` / `AjaxJson`. - `Bitrix\Main\Engine\Response\Render\View` / `Render\Component` / `Render\Extension` — HTML for HTTP routes (`renderView` / `renderComponent` / `renderExtension`). - `Bitrix\Main\Engine\Response\Component` — JSON component payload from `renderComponentAjax()` (not the same class as `Render\Component`). - `Bitrix\Main\Engine\Response\BFile` / `File` — file delivery (`BFile::createByFileId()` for `b_file`). - `Bitrix\Main\Engine\Response\ResizedImage` — resized image (`createByImageId($id, $w, $h)`); never take width/height raw from the request. - `Bitrix\Main\Engine\Response\Zip\Archive` — ZIP stream. - `Bitrix\Main\Engine\Response\OpenDesktopApp` / `OpenMobileApp` — deep-link into native Bitrix apps. Controller helpers: ```php return $this->renderView('list', ['items' => $items]); // => /local/modules/vendor.module/views/list.php return $this->renderComponent('vendor:post.list', '.default', ['IBLOCK_ID' => 12]); return $this->renderExtension('vendor.post.list', ['items' => $items]); return $this->redirectTo('/posts/'); ``` Prefer these helpers / typed responses over manual `header()` / `json_encode()` (see `bitrix-request-response`). ## Scope (AJAX / REST / CLI) - **AJAX**: `/bitrix/services/main/ajax.php?action=...` or `BX.ajax.runAction('...', {})`. Available when controller is declared and `controllers` exists in `.settings.php`. - **REST**: requires `restIntegration.enabled = true` + `rest` module. - **CLI**: possible with `ActionFilter\Scope` when calling controllers from commands. Different scopes need different filter sets. CSRF does not apply to REST by default — add an explicit strategy. ## Checklist - [ ] Controller is thin: orchestration only; business logic in a service. - [ ] Filters use attributes by default; `configureActions` only when needed. - [ ] `getDefaultPreFilters()` extends parent when overridden. - [ ] Current user via `CurrentUser` / `getCurrentUser()`, not global `$USER`. - [ ] Dependencies via **action parameters**, not controller constructor. - [ ] Input via Request DTO + `ValidationParameter` in `getAutoWiredParameters()` when the contract is non-trivial. - [ ] Errors via `$this->addError` / `addErrors`, not exceptions for normal failures. - [ ] Return type explicit: `array`, `HttpResponse`, or `renderXxx` / `redirectTo`. -
filters.md 5 KB
# Filters and attributes ## Default Prefilters By default, actions get: `Authentication` + `HttpMethod([GET, POST])` + `Csrf`. When overriding `getDefaultPreFilters()`, **extend** `parent::getDefaultPreFilters()` — do not rebuild the base protection from scratch without reason. ## Action Filters Predefined filters: - `ActionFilter\Authentication` — requires an authorized user (401 without redirect). - `ActionFilter\Csrf` — `sessid`/`X-Bitrix-Csrf-Token` check. **Limited to `SCOPE_AJAX`** (`listAllowedScopes()`); does not run for REST/CLI scopes. - `ActionFilter\HttpMethod([...])` — method restriction. - `ActionFilter\CloseSession` — closes session before action (parallel AJAX). - `ActionFilter\ContentType(['application/json'])` — allowed `Content-Type`. - `ActionFilter\Scope($scope)` — restricts call to a specific scope (ajax/rest/cli). - `ActionFilter\Cors` — CORS headers for cross-origin AJAX. - `ActionFilter\AccessCheck` / `#[ActionAccess]` — module ACL via `AccessibleController` (see below). - `ActionFilter\Token` — signed entity token in request headers (`X-Bitrix-Sign-Entity` / `X-Bitrix-Sign-Token` via `ActionFilter\Service\Token`). Niche; prefer CSRF + rights for ordinary AJAX. If a built-in filter is missing, write a custom `ActionFilter\Base` — do not copy-paste checks into every action. ## `#[ActionAccess]` (module ACL) When the module already has an `access` controller (`Bitrix\Main\Access\AccessibleController` / `BaseAccessController`), check the action with `#[ActionAccess]` instead of a hand-rolled rights filter. The controller **must** implement `Bitrix\Main\Engine\Contract\AccessCheckControllerInterface` and return the access controller from `getAccessController()`. Otherwise `AccessCheck` throws `SystemException`. Default strategy is `ItemIdFromRequestStrategy`: it reads a request key (default `id`) and calls `checkByItemId($action, $itemId)`. Override the key with `strategyArgs`. ```php use Bitrix\Main\Access\AccessibleController; use Bitrix\Main\Engine\ActionFilter\Access\ItemIdFromRequestStrategy; use Bitrix\Main\Engine\ActionFilter\Attribute\Access\ActionAccess; use Bitrix\Main\Engine\Contract\AccessCheckControllerInterface; use Bitrix\Main\Engine\CurrentUser; final class Post extends Controller implements AccessCheckControllerInterface { public function getAccessController(): AccessibleController { return PostAccessController::getInstance((int)CurrentUser::get()->getId()); } #[ActionAccess( action: PostActionDictionary::VIEW, // string or UnitEnum (BackedEnum → value, else → name) strategy: ItemIdFromRequestStrategy::class, strategyArgs: ['itemIdRequestKey' => 'id'], )] public function getAction(int $id, PostService $postService): array { // ... } } ``` `#[ActionAccess]` does **not** set HTTP 403 by default (`setHttpStatus` is only on `AccessCheck` itself, default `false`). The action is rejected with an engine error; set status yourself in a custom strategy/`AccessCheck` if the HTTP code matters. Custom strategies implement `AccessCheckStrategyInterface::create()` + `check()`. Do not use `#[ActionAccess]` unless the module actually has an access controller — for simple auth keep `Authentication` / a small `ActionFilter\Base`. ## PHP 8 Attribute Filters (preferred) ```php use Bitrix\Main\Engine\ActionFilter; use Bitrix\Main\Engine\ActionFilter\Attribute\Rule\Prefilters; use Bitrix\Main\Engine\ActionFilter\Attribute\Rule\HttpMethod; use Bitrix\Main\Engine\ActionFilter\Attribute\Rule\Authentication; use Bitrix\Main\Engine\ActionFilter\Attribute\Rule\Csrf; use Bitrix\Main\Engine\ActionFilter\Attribute\Rule\EnablePrefilters; use Bitrix\Main\Engine\ActionFilter\Attribute\Rule\DisablePrefilters; final class Post extends Controller { #[HttpMethod([HttpMethod::METHOD_GET])] #[DisablePrefilters([ActionFilter\Csrf::class])] public function listAction(): array { /* ... */ } #[Authentication] #[HttpMethod([HttpMethod::METHOD_POST])] #[Csrf] public function createAction(string $title): array { /* ... */ } #[DisablePrefilters([ActionFilter\Authentication::class, ActionFilter\Csrf::class])] #[EnablePrefilters([ new ActionFilter\HttpMethod([ActionFilter\HttpMethod::METHOD_GET]), ])] public function publicPingAction(): array { return ['ok' => true]; } } ``` Controller-level defaults via `getDefaultPreFilters()` / `getDefaultPostFilters()`. Use `#[EnablePrefilters]` / `#[DisablePrefilters]` to adjust inherited defaults per action. ### `configureActions()` (compatibility) Use when attributes are insufficient or when patching legacy controllers: ```php public function configureActions(): array { return [ 'get' => [ '+prefilters' => [new ActionFilter\HttpMethod([ActionFilter\HttpMethod::METHOD_GET])], '-prefilters' => [ActionFilter\Csrf::class], ], ]; } ``` Format keys: `prefilters` (replace), `+prefilters` (add), `-prefilters` (remove by FQCN), `postfilters`.
-
-
SKILL.md 3 KB
--- name: bitrix-controllers description: "Engine Controller/JsonController: thin actions, filter attributes, CurrentUser, errors. Use for AJAX/REST/routed endpoints." --- # Bitrix Controllers Baseline: **main 23.0+**. Features newer than baseline are marked **Since**. Progressive disclosure: open **only** the rule files that match the task. Do not read every `rules/*.md`. ## How to use 1. Identify the layer the task touches. 2. Open the matching `rules/*.md` below. 3. Prefer framework-native Bitrix patterns over custom abstractions. ## Choose a rule file ### When to read `rules/basics.md` Read `rules/basics.md` (`Location, thin controller, autowire`) when the task involves: - Location and Naming - Minimal Controller - Action Parameter Autowiring - Controller Lifecycle - Additional Autowire Types - Front-end Call ### When to read `rules/filters.md` Read `rules/filters.md` (`Filters and attributes`) when the task involves: - Default Prefilters - Action Filters - PHP 8 Attribute Filters (preferred) - `#[ActionAccess]` / `AccessCheck` (module ACL) ### When to read `rules/errors-response.md` Read `rules/errors-response.md` (`Errors, responses, scope`) when the task involves: - Errors - Response Types - Scope (AJAX / REST / CLI) - Checklist ## Cross-cutting invariants (apply regardless of rule file) - Never configure the same action **both** via filter attributes and `configureActions()` — the controller fails with `Invalid configuration of actions`. - Do not register one controller both as an HTTP route target (`/local/routes/web.php`) and in the AJAX `controllers.defaultNamespace` of `.settings.php`. Keep separate `Web\*` and `Ajax\*` controllers, each with its own `getAutoWiredParameters()`. - Rendering helpers `renderView()` / `renderComponent()` / `renderExtension()` — **Since main 25.700.0**. They return HTML (`HttpResponse`-based) and are for HTTP routes only; `BX.ajax.runAction()` expects JSON. For AJAX use `renderComponentAjax()` — JSON with `html`, `assets`, `additionalParams`, `componentResult`. `renderExtension()` / `renderView()` / `renderComponent()` accept `withSiteTemplate: false` to skip the site template. `renderExtension()` requires `controllerEntrypoint` in the extension's `config.php`; it renders in the browser (not SSR). - `PageNavigation` autowire (global, nav id `nav`) accepts a page size only within 1–50 (`setPageSizes(range(1, 50))`); an out-of-range `size` is silently ignored and the default 20 is used. - Request DTOs are wired with `new ValidationParameter(...)` in `getAutoWiredParameters()` — **not** a `#[ValidationParameter]` attribute (that class does not exist). - `#[ActionAccess]` requires the controller to implement `AccessCheckControllerInterface`. Without it the engine throws `SystemException`. ## Checklist - [ ] Opened only the rule file(s) needed for this task. - [ ] Followed DI / `/local/` / security canons from `AGENTS.md`. - [ ] Respected the cross-cutting invariants above (no attribute + `configureActions()` mix, separate Web/Ajax controllers).
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.