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

How Light fits together

The parts of Light, a request to a Light page, the three ways to plug in, and the public API, as diagrams.

Light is a set of Magento modules. Your module talks to it the Magento way: DI items in pools, blocks in named containers, and a handful of @api interfaces. This page draws how those parts connect. The recipes show how to use each one.

The parts

  • Light core is the 14 modules of Surface::CORE_MODULES. They ship together with one API version.
  • Packs (packages/) are modules outside core that fill core's pools and containers: Mrx_CountryNl holds what is Dutch, Mrx_PaymentsMollie and Mrx_PaymentsPaynl connect a payment provider. Core names none of them, so a shop installs the packs it needs. The doctor and the compatibility gate check a pack like any module (Ship it as a pack).
  • The catalogue lists every extension point: the pools your di.xml adds items to, the layout containers and handles, the seven editor forms, the public JS modules and the events. Extension targets prints it.
  • The compatibility gate reads the Light API range each module declares and hides what an out-of-range module adds, before any pool is read (Versioning).
  • The -hyva modules hold the Hyvä storefront code of core modules, so Light itself never needs Hyvä (Hyvä code in a -hyva module).
  • The checks keep all of it honest: the doctor reads what every module registers, the surface snapshot keeps the API stable, and the guards prove a module landed without a change to core (Testing).

A request to a Light page

Every Light page is a controller on the admin front name light. Nothing of it answers before a complete sign-in: signed in and, where Magento_TwoFactorAuth is on, past the second factor (Model\Auth\AdminAccess). Where admins sign in with an Adobe ID (Magento_AdminAdobeIms), Magento skips its own second factor and Light counts that sign-in as complete. The gate answers 404 for a controller whose module is off or hidden, and the blocks read the pools through ModuleGate, so an item of a hidden module never renders:

Light asks Magento_TwoFactorAuth itself whether the second factor is done, so a module that skips Magento's two-factor check without granting the session keeps Light closed while stock pages open. Light keeps sending you to the two-factor screen has the fix. The same check decides whether Light reads anything of the admin: their own theme, their pinned apps and their counts wait for the complete sign-in too.

Emails and documents

Mrx_Documents gives every customer email one look from one brand kit: the store's logo, the owner's accent colour, the footer text and the company details, in one of the formats of the formats pool. A module writes only the body; the stock header and footer includes lead into Light's shell:

ApplyEmailTexts puts the subject, opening text and button text the owner saved in Settings > Customer emails into a body of the emails pool as it loads, MapShellIds maps the stock header and footer ids to Light's, SwapCssTokens fills the __MRX_*__ tokens of the email CSS with the brand kit of the mail's store, and Emogrifier inlines the result. When the mail is sent, AddPlainTextPart adds a plain-text part made from the HTML. RememberEmailContext notes the template id and what the vars say about the order while the mail is built, and KeepSentCopy stores an exact copy of a customer mail about an order in mrx_documents_sent_email once it went out. The order page's timeline reads those copies and links each mail's line to it (Copies of sent mails). Your module's email and document is the contract.

The same brand kit prints the PDFs. Every stock caller of an invoice, credit memo or packing slip PDF (the print buttons, the mass actions with "Print All", Light's "Print invoices", the accountant ZIP) calls getPdf(), and an around plugin answers it without the stock drawing code:

A document type in the documents pool supplies the data and the body template, the item_lines pool the rows of the items table, and the document_sections pool blocks from other modules after the parties, items or totals. The order confirmation has no stock PDF, so only the download route and DocumentRendererInterface reach it; the picklist reads bin locations from the pick_locations pool. Engine renders each store view's documents in that store's language, at most 25 to a dompdf run, and merges the runs. DocumentPdf hands the bytes back as a \Zend_Pdf, whose render() returns them as they are and whose pages "Print All" merges with the packing slips, which are dompdf documents too.

The same PDFs reach the customer two more ways. AttachingSenderBuilder replaces the SenderBuilder of Magento's seven sales senders and adds the invoice, credit memo or order confirmation PDF to the email. One setting, mrx_documents/invoice/send_with, decides which email carries the invoice: the shipping confirmation (the default), "Payment received" or none, and the refund email carries the credit memo. mrx_documents_invoice_mailed remembers which invoices went out, so each travels once. Returns' own EmailSender puts the return slip on the customer's approval mail with MessageAttacher, the class the builder attaches with. In My Account and the guest order view, ServePdfOnPrint answers "Print invoice" and "Print refund" with the PDF once the stock controller has checked the order (PDFs on customer emails).

Customer text in two forms

Dutch and German address a customer two ways, and the merchant picks one per language of a channel. Magento builds the dictionary of a store in layers, and the variant files take the pack's slot:

PreferModulePhrases runs first: for the phrases in the pool prefer_phrases it writes the named module's row over the language pack's, where a pack gets a phrase wrong for Light (German "Returns" as "Details anzeigen"). FormOfAddressRows is an afterGetDictionary plugin on Magento\Framework\App\Language\Dictionary. In a customer area (storefront, REST, GraphQL) and in store emulation, it merges the variant file for the store's form from every enabled module into the pack's rows. Every mail and PDF renders in store emulation, so they follow the form too. The admin keeps its own dictionary. A theme's CSV and the merchant's inline edits load later and win. js-translation.json is one static file per theme and locale, so KeepJsDictionaryNeutral builds it from the base rows only (Channels and store views).

Countries

Core knows three things about a country: the home country in Business details, the One Stop Shop for an EU home country, and the identifiers a country module supplies through the pool country_rules. The Taxes page runs on Magento's own tax tables, so a shop anywhere can add its rates, switch "Prices include tax" and say it doesn't charge tax. A country module adds what is specific to its country in three places: an item in country_rules for the business registration number, the tax ID and the postcode; cards in the containers mrx.settings.taxes.preset, mrx.settings.taxes.cross_border, mrx.settings.business.registration and mrx.settings.payments.providers; and block arguments that change the wording of a core card. Carriers and legal page drafts are pools too (carriers, legal_page_templates and legal_page_types). The Dutch parts live in the pack Mrx_CountryNl (packages/module-country-nl): the Dutch VAT setup, the KvK number and Dutch VAT number checks, the postcode, the address order, PostNL and the Dutch tracking links, and the legal page drafts. A shop in another country runs core on its neutral defaults, and the doctor rule country_pack warns when a known pack for the shop's country isn't enabled. A settings page from your system.xml shows each place.

Locations and pickup

A location is a source of Magento's Multi-Source Inventory (MSI). Mrx_Locations adds Settings > Locations and reads the sources for the rest of Light; every write goes through MSI's own services, so a module that reads MSI sees what Light did:

  • Simple mode. With MSI off, or MSI on and one enabled source, a shop has one location: the business address. Every screen and every write is the single-quantity one, and pickup is Light's own carrier at the business address (carriers/mrxpickup).
  • Locations mode. The first "Add location" runs the conversion: the business address becomes a source, the stock of the default source is copied to it, a stock Light makes (the Light stock) links both, the open orders' reservations and every channel move to it, and default is set to 0. From then on stock is kept per location, a shipment carries the location it ships from, and pickup is Magento's In-Store Pickup at the locations that offer it (with Magento's in-store pickup modules off, Light's own carrier stays at the business address).
  • Module graph. Mrx_Light ← Mrx_Settings ← Mrx_Locations ← Mrx_Catalog, Mrx_Orders, Mrx_Home. Settings declares the internal PickupProviderInterface and its default; Locations replaces the preference, so Settings never names Locations. Documents and Returns read where a parcel shipped from through Orders.

Locations and pickup has the contract and the plugins Light adds on MSI.

Three ways to plug in

  • Pattern A keeps the module you bridge untouched. Every call into it sits under Model/Backend/, and a unit test holds that line.
  • Pattern B1 is XML only: scalar DI items on Light classes and layout on Light handles. Without Light, Magento ignores them, so the module installs on any shop.
  • Pattern B2 is the PHP that needs Light types. Its range guard leaves it unregistered on a shop without Light, or with a Light outside its range.
registration.php
composer.json
module.xml
di.xml
di.xml
routes.xml
Index.php
View.php
MassStatus.php
MassDelete.php
Requests.php
RequestsProvider.php
PendingRequests.php
StatusBadge.php
RequestView.php
CustomerRequestCard.php
layout
templates
request-view.js
nl_NL.csv
BoundaryTest.php

Getting started builds both in about 15 minutes.

The public API

Everything your PHP may name is @api. The diagrams group the types by the job they do; API reference lists every type with every member, and the doctor reports private_api for anything else.

A ? in front of a return type in the code means the method may return null: getBadgeValue(): ?Badge answers null at 0. The diagrams leave the ? out.

Last updated on

On this page