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_Helloexample. - 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, andbin/magento setup:upgrade --keep-generated. Its plugins onCustomer\Model\Urland, on the storefront, on the reCAPTCHA check needgenerated/metadata/staticcache/*.phpand 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/defaultor inherits from it; here every store view does throughMageRex/studio. Light itself needs no Hyvä.
The bridge, surface by surface
| Surface | Bridge files | Extension point |
|---|---|---|
| Nav item and counter | etc/adminhtml/di.xml, Model/PendingRequests.php | nav items, BadgePool providers |
| Home to-do, pin and palette count | etc/adminhtml/di.xml | a TodoList item with badge |
| Light list with row and bulk actions | Controller/Adminhtml/Accountrequests/Index.php, Model/IndexTable/RequestsProvider.php, layout | ProviderPool, RowActionsInterface |
| Detail page | Controller/Adminhtml/Accountrequests/View.php, ViewModel/RequestView.php, template | AbstractPage |
| Status change and delete | Controller/Adminhtml/Accountrequests/MassStatus.php, MassDelete.php | bulk action routes |
| Stock grid to Light list | etc/adminhtml/di.xml | Redirect\Map |
| Settings page | etc/adminhtml/di.xml | Page\Pool, ConfigSections, a Settings card |
| App | etc/adminhtml/di.xml | Registry apps, claims, hidden_modules |
| Palette | etc/adminhtml/di.xml | Actions, one Destinations item |
| Customer card | view/adminhtml/layout/light_customers_view.xml | mrx.customer.aside.bottom |
| Role access | etc/di.xml | RoleAreas |
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:
"disrex/module-request-an-account": "^1.6",A unit test keeps every reference to the module's classes under Model/Backend/:
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.
Nav item and counter
"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):
<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

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.tschecks 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=jsonIt reports "errors": []: no private_api, unknown_resource, unknown_container or unknown_reference.
- The guards.
npm run guard:zero-core-editexits 0: nothing underapp/code/Mrxchanged.composer show disrex/module-request-an-accountstill reports 1.6.0, anddiff -ragainst a fresh copy of the 1.6.0 dist finds no change undervendor/disrex/module-request-an-account. - The gate. With
extra.mrx-light-apiand therequireonmrx/module-lightset to^9.0for a moment (the range guard stays^0.4, so the bridge stays registered), the doctor reports twoapi_constrainterrors, 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 callsBadgeCacheInterface::invalidate()for it. The bridge's own status changes and deletes do. - The doctor's
acl_unreachablecounts 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-core0.7.4 names a stylesheet it doesn't ship, so every admin page logged a 404. The bridge removes it from the admin head inview/adminhtml/layout/default.xmluntil 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 itscomposer.jsonallows PHP 8.1. exit;inObserver/RedirectToCustomRegistration.php.- Its
module.xmlhas no sequence onDisrex_Core.
Last updated on