Light API 0.4.0 is a public beta: a minor release such as 0.5.0 may still break. What that means for your module
Light
Developer guide

Case study: disrex/module-request-an-account

How the first real module joined Light with no change to Light core or to the module.

disrex/module-request-an-account was the first real module plugged into Light on the finished backbone. Its bridge, Disrex_RequestAnAccountLight, was built with no change to Light core and none to the module itself. This page walks through it, from install to sign-off.

Why this module went first

  • It had to prove pattern A, a bridge, on a module Light must not change. Pattern B has its own proof, the Acme_Hello example.
  • The Light API and the module release on their own schedules, and the bridge pins both.
  • It is exactly what the zero-core-edit guard has to prove: every line of glue sits outside app/code/Mrx, and the module stays untouched.

The module

A B2B lead form. A business customer fills in the storefront form; the module stores a row in disrex_registration_request with the status new and emails the shop. The answers sit in form_fields as a JSON list of {field_label, field_value, field_type}, where field_value can be a list for checkboxes. There is no approval flow, so Light offers "Mark handled" and "Reopen", not approve and reject.

It comes as disrex/module-request-an-account 1.6.0 from packages.disrex.nl and needs disrex/module-core. In the stock admin its grid and settings hang under the Disrex menu, which no Light section or role area covers: Apps would file it under "Other", and staff would never see it.

Prerequisites

  • The package, required with Composer from packages.disrex.nl, and bin/magento setup:upgrade --keep-generated. Its plugins on Customer\Model\Url and, on the storefront, on the reCAPTCHA check need generated/metadata/staticcache/*.php and those interceptors cleared after install (Troubleshooting).
  • A Hyvä store view: its storefront templates are built for Hyvä. A store view qualifies when its theme is Hyva/default or inherits from it; here every store view does through MageRex/studio. Light itself needs no Hyvä.

The bridge, surface by surface

SurfaceBridge filesExtension point
Nav item and counteretc/adminhtml/di.xml, Model/PendingRequests.phpnav items, BadgePool providers
Home to-do, pin and palette countetc/adminhtml/di.xmla TodoList item with badge
Light list with row and bulk actionsController/Adminhtml/Accountrequests/Index.php, Model/IndexTable/RequestsProvider.php, layoutProviderPool, RowActionsInterface
Detail pageController/Adminhtml/Accountrequests/View.php, ViewModel/RequestView.php, templateAbstractPage
Status change and deleteController/Adminhtml/Accountrequests/MassStatus.php, MassDelete.phpbulk action routes
Stock grid to Light listetc/adminhtml/di.xmlRedirect\Map
Settings pageetc/adminhtml/di.xmlPage\Pool, ConfigSections, a Settings card
Appetc/adminhtml/di.xmlRegistry apps, claims, hidden_modules
Paletteetc/adminhtml/di.xmlActions, one Destinations item
Customer cardview/adminhtml/layout/light_customers_view.xmlmrx.customer.aside.bottom
Role accessetc/di.xmlRoleAreas

How the parts connect:

The skeleton

registration.php carries the range guard, because the bridge has Light PHP; composer.json declares the same range in extra.mrx-light-api and on mrx/module-light, and requires the module:

app/code/Disrex/RequestAnAccountLight/composer.json
"disrex/module-request-an-account": "^1.6",

A unit test keeps every reference to the module's classes under Model/Backend/:

app/code/Disrex/RequestAnAccountLight/Test/Unit/BoundaryTest.php
self::assertSame([], $offenders, 'Move these references under Model/Backend/');

The backend service, Model/Backend/Requests.php, is the only class that talks to the module: it counts, reads, changes and deletes requests through the module's own resource model, so the module's model events still fire.

"Account requests" under Customers, with a counter of the requests to review that rolls up into Customers while its section is closed (A nav item with a counter):

app/code/Disrex/RequestAnAccountLight/etc/adminhtml/di.xml
<item name="rollup" xsi:type="boolean">true</item>

The merchant sees "Customers 5" while Customers is closed, "Account requests 5" inside it.

Home to-do, pin and palette

A Home to-do reads the same count (Home to-dos, setup steps and widgets). A pinned app and the "Go to" entry show it without any code. The merchant sees "5 account requests to review" on Home, linking to the list's "To review" tab.

Light list with row and bulk actions

light/accountrequests/index lists the requests with the tabs "To review", "Handled" and "All", search on name, email and company, and "Mark handled", "Reopen" and "Delete" as bulk and row actions (Light lists).

Detail page

A request opens with its contact details, the answers (a checkbox answer reads "Groothandel, Webshop"), the comment, and "Open customer" when a customer with that email exists (Your own list and detail pages).

Status change and delete

MassStatus and MassDelete answer the list and the detail page, and drop the nav count, so the nav, the pin, the palette and Home update on the next page.

Stock grid to Light list

The route map sends the stock grid requestanaccount/request/index to the Light list in simple mode; with mrx_stock=1 it stays, and the banner says "Back to Account requests" (The advanced view, the route map and header actions).

Settings page

A settings page from the module's own system.xml, with the everyday fields named one by one, warm labels and notes, one input per language for the welcome message, and an Advanced section that shows the form page address, the thank-you page address, the terms link and the form builder switch, read-only (A settings page from your system.xml). A Settings card under Selling points at it.

App

The Apps card "Account requests" sits under Customers, opens the Light list and links the settings page; the stock "Configuration" screen is hidden, and Disrex_Core is not an app (An app, its category and pins).

Palette

"Review account requests" under Actions, and "Account request settings" under Go to (The command palette).

Customer card

The customer page shows "Account request" with the date, the status and "View request" when that customer's email sent one (Order and customer pages).

Role access

The module's resources join the Customers area, so the Staff preset and custom roles with Customers reach the list, and existing Staff roles catch up on the next setup:upgrade (Users and permissions).

What the admin sees

The navigation with its counters: Orders counts the orders to ship, and Customers the account requests to review, rolled up while the section is closed

What stays in the advanced view

The form builder (a serialized list of fields edited in a modal), the badge, benefits, quote, contact and success steps, the colours, the "type for" setting and the SEO keywords and canonical URL stay in the stock section, behind the hint. They are set up once, by whoever builds the form, not in daily work.

The checks

  • The storefront. On the Hyvä store view, the request form renders with the Hyvä layout, a submitted request lands on the "To review" tab, and the nav count goes up by one.
  • The browser proof. tests/playwright/pilot-request-an-account.spec.ts checks the counts everywhere, the list, the detail page, the route map, the settings page, Apps, the palette, the customer card, role access for an existing Staff role, and the storefront form.
  • The doctor. The bridge uses only the public API:
$ bin/magento mrx:light:doctor --module=Disrex_RequestAnAccountLight --format=json

It reports "errors": []: no private_api, unknown_resource, unknown_container or unknown_reference.

  • The guards. npm run guard:zero-core-edit exits 0: nothing under app/code/Mrx changed. composer show disrex/module-request-an-account still reports 1.6.0, and diff -r against a fresh copy of the 1.6.0 dist finds no change under vendor/disrex/module-request-an-account.
  • The gate. With extra.mrx-light-api and the require on mrx/module-light set to ^9.0 for a moment (the range guard stays ^0.4, so the bridge stays registered), the doctor reports two api_constraint errors, Home shows the red banner in developer mode, and the account requests disappear from Customers, Home, the customer page and the list route, which answers 404.

What the pilot taught

The pilot found no gap in the Light API: every surface above uses only @api types and catalogued pools, and the guard base never moved for it. It did show three edges:

  • A request sent from the storefront reaches the count within its ttl, 60 seconds, not at once: the module saves through its own resource model without an event prefix, so nothing calls BadgeCacheInterface::invalidate() for it. The bridge's own status changes and deletes do.
  • The doctor's acl_unreachable counts a module as reached once any of its resources is. The bridge's "Go to" entry for its settings sits in the Settings area, so the rule stayed quiet before the Customers-area row existed; the bridge's unit and browser tests check that row instead.
  • disrex/module-core 0.7.4 names a stylesheet it doesn't ship, so every admin page logged a 404. The bridge removes it from the admin head in view/adminhtml/layout/default.xml until module-core is fixed.

The pilot against the "Built for Light" rules

  • Place, don't add: the list sits under Customers as "Account requests", named after the job; no new top-level item, no new category.
  • Use the smallest surface: a Home to-do instead of a widget, and a customer card that renders nothing without a request.
  • Counts mean work: the counter counts only requests to review, is null at 0, and uses attention.
  • Simple mode shows everyday settings: nine fields named one by one; the form builder and styling stay in the advanced view.
  • Pins belong to the merchant: no default pin.
  • Warm copy: in Dutch "Markeren als afgehandeld" for the action and "Afgehandeld" for the status, and an empty state that says what will appear.

Moving to pattern B

The module is Disrex's own, so its Light glue could later move into the module itself. That release keeps every identifier and route path of the bridge (Versioning) and declares:

"conflict": {"disrex/module-request-an-account-light": "*"}

light_collision flags a bridge left installed next to it.

Outside Light

These go to Disrex and didn't block the pilot:

  • The module uses public const string, which needs PHP 8.3, while its composer.json allows PHP 8.1.
  • exit; in Observer/RedirectToCustomRegistration.php.
  • Its module.xml has no sequence on Disrex_Core.

Last updated on

On this page