The Admin
A UI component grid and form for the entity, behind one ACL resource, reached from a menu entry.
<dataProvider class="Magento\Framework\View\Element\UiComponent\DataProvider\DataProvider" name="acme_locator_location_listing_data_source">A UI component is an admin grid or form described in XML and rendered in the browser by Magento's JavaScript. You do not write the grid's HTML, its paging or its filters. You declare a data source and some columns.
The listing
The grid lives in view/adminhtml/ui_component/acme_locator_location_listing.xml. One string ties it together: the data source name acme_locator_location_listing_data_source. It appears as the provider at the top of the file, as the dataSource and dataProvider name, and as the key of the collections entry in etc/di.xml that says which collection to load. All four must match exactly.
The page it renders on
An admin controller returns a page, and the layout file view/adminhtml/layout/acme_locator_location_index.xml puts <uiComponent name="acme_locator_location_listing"/> into the content container. The form is a second component, acme_locator_location_form.xml, with its own data provider and a submitUrl pointing at your save controller.
Permission and the menu
One ACL (access control list) resource id, Acme_Locator::location, is declared in etc/acl.xml and then used three times: as the controller's ADMIN_RESOURCE constant, as the listing's aclResource, and as the resource attribute of the <add> element in etc/adminhtml/menu.xml.
UI component and layout XML are cached: run bin/magento cache:flush. A new di.xml entry needs bin/magento setup:di:compile if compiled code already exists.
Example
<!-- app/code/Acme/Locator/view/adminhtml/ui_component/acme_locator_location_listing.xml -->
<listing xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.xsd">
<argument name="data" xsi:type="array">
<item name="js_config" xsi:type="array">
<item name="provider" xsi:type="string">acme_locator_location_listing.acme_locator_location_listing_data_source</item>
</item>
</argument>
<settings>
<spinner>location_columns</spinner>
<deps>
<dep>acme_locator_location_listing.acme_locator_location_listing_data_source</dep>
</deps>
</settings>
<dataSource name="acme_locator_location_listing_data_source" component="Magento_Ui/js/grid/provider">
<settings>
<updateUrl path="mui/index/render"/>
</settings>
<aclResource>Acme_Locator::location</aclResource>
<dataProvider class="Magento\Framework\View\Element\UiComponent\DataProvider\DataProvider"
name="acme_locator_location_listing_data_source">
<settings>
<requestFieldName>location_id</requestFieldName>
<primaryFieldName>location_id</primaryFieldName>
</settings>
</dataProvider>
</dataSource>
<listingToolbar name="listing_top">
<bookmark name="bookmarks"/>
<columnsControls name="columns_controls"/>
<filters name="listing_filters"/>
<paging name="listing_paging"/>
</listingToolbar>
<columns name="location_columns">
<column name="location_id" sortOrder="10">
<settings>
<label translate="true">ID</label>
</settings>
</column>
<column name="name" sortOrder="20">
<settings>
<label translate="true">Name</label>
</settings>
</column>
<column name="city" sortOrder="30">
<settings>
<label translate="true">City</label>
</settings>
</column>
</columns>
</listing>
<!--
What changes in the admin: the page rendered by the Locations controller
shows a sortable, filterable, paged grid of acme_locator_location rows,
with column settings and filters remembered per admin user by the
bookmark component. Users whose role lacks Acme_Locator::location get no
data from it.
Apply it with:
bin/magento cache:flush
-->When to use it
- Give merchants a grid to manage a custom entity instead of editing rows in the database.
- Add a permission so warehouse staff can see locations but not delete them.
- Put a new admin screen in the right menu section with a breadcrumb and a page title.
More examples
di.xml: the preferences and the grid collection
Three jobs in one file. The preferences say which class to build when something asks for an interface, which is what makes constructor injection of LocationRepositoryInterface work. The virtual type is a configured instance of the framework's SearchResult collection with no new PHP class; it exists only so the grid has something that can both query the table and report a total count. The <type> block registers that virtual type under the data source name, which is how the generic DataProvider finds it. Run bin/magento setup:di:compile after editing di.xml, and bin/magento cache:flush to drop the cached configuration.
<?xml version="1.0"?>
<!-- app/code/Acme/Locator/etc/di.xml -->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
<preference for="Acme\Locator\Api\LocationRepositoryInterface"
type="Acme\Locator\Model\LocationRepository"/>
<preference for="Acme\Locator\Api\Data\LocationInterface"
type="Acme\Locator\Model\Location"/>
<preference for="Acme\Locator\Api\Data\LocationSearchResultsInterface"
type="Acme\Locator\Model\LocationSearchResults"/>
<virtualType name="Acme\Locator\Model\ResourceModel\Location\Grid\Collection"
type="Magento\Framework\View\Element\UiComponent\DataProvider\SearchResult">
<arguments>
<argument name="mainTable" xsi:type="string">acme_locator_location</argument>
<argument name="resourceModel" xsi:type="string">Acme\Locator\Model\ResourceModel\Location</argument>
</arguments>
</virtualType>
<type name="Magento\Framework\View\Element\UiComponent\DataProvider\CollectionFactory">
<arguments>
<argument name="collections" xsi:type="array">
<item name="acme_locator_location_listing_data_source" xsi:type="string">Acme\Locator\Model\ResourceModel\Location\Grid\Collection</item>
</argument>
</arguments>
</type>
</config>
<!--
Acme\Locator\Model\LocationSearchResults is three lines, and it has to
exist: Magento\Framework\Api\SearchResults does not implement
LocationSearchResultsInterface, so pointing the preference straight at it
makes getList(): LocationSearchResultsInterface throw a TypeError.
class LocationSearchResults extends \Magento\Framework\Api\SearchResults
implements \Acme\Locator\Api\Data\LocationSearchResultsInterface
{
}
-->acl.xml, and the menu entry that points at the same id
Every resource must be nested under an existing one, here under Stores > Settings, because the tree in this file is the tree of checkboxes in System > Permissions > User Roles. The titles are what a merchant sees there. The matching menu entry is a single line in app/code/Acme/Locator/etc/adminhtml/menu.xml: <add id="Acme_Locator::location" title="Pickup Locations" translate="title" module="Acme_Locator" sortOrder="50" parent="Magento_Backend::stores_settings" action="acme_locator/location/index" resource="Acme_Locator::location"/>. The resource attribute hides the menu item from roles that lack the permission. Both files are config, so run bin/magento cache:flush.
<?xml version="1.0"?>
<!-- app/code/Acme/Locator/etc/acl.xml -->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:Acl/etc/acl.xsd">
<acl>
<resources>
<resource id="Magento_Backend::admin">
<resource id="Magento_Backend::stores">
<resource id="Magento_Backend::stores_settings">
<resource id="Acme_Locator::location" title="Pickup Locations"
translate="title" sortOrder="50">
<resource id="Acme_Locator::location_save" title="Save Pickup Location"
translate="title" sortOrder="10"/>
<resource id="Acme_Locator::location_delete" title="Delete Pickup Location"
translate="title" sortOrder="20"/>
</resource>
</resource>
</resource>
</resource>
</resources>
</acl>
</config>The admin controller behind the grid
The ADMIN_RESOURCE constant is the whole access check. Magento\Backend\App\Action inherits it from Magento\Backend\App\AbstractAction, which defines it as Magento_Backend::admin, so a controller that forgets to override it is open to every admin user with any permission at all: a real finding in code review. Either page factory produces the right result here, because Magento_Backend's etc/adminhtml/di.xml sets Magento\Framework\View\Result\PageFactory's instanceName to Magento\Backend\Model\View\Result\Page in the admin area; core admin controllers inject the plain Magento\Framework\View\Result\PageFactory and still get the page that carries the admin menu and breadcrumbs. The grid itself is added by app/code/Acme/Locator/view/adminhtml/layout/acme_locator_location_index.xml, whose body contains <referenceContainer name="content"><uiComponent name="acme_locator_location_listing"/></referenceContainer>.
<?php
// app/code/Acme/Locator/Controller/Adminhtml/Location/Index.php
declare(strict_types=1);
namespace Acme\Locator\Controller\Adminhtml\Location;
use Magento\Backend\App\Action;
use Magento\Backend\App\Action\Context;
use Magento\Backend\Model\View\Result\Page;
use Magento\Backend\Model\View\Result\PageFactory;
use Magento\Framework\App\Action\HttpGetActionInterface;
class Index extends Action implements HttpGetActionInterface
{
public const ADMIN_RESOURCE = 'Acme_Locator::location';
public function __construct(
Context $context,
private readonly PageFactory $resultPageFactory
) {
parent::__construct($context);
}
public function execute(): Page
{
$resultPage = $this->resultPageFactory->create();
$resultPage->setActiveMenu('Acme_Locator::location');
$resultPage->getConfig()->getTitle()->prepend(__('Pickup Locations'));
return $resultPage;
}
}
// What the reader sees: Stores > Settings > Pickup Locations in the admin
// menu loads admin/acme_locator/location/index, status 200, with the
// breadcrumb set and the grid rendered inside the page. An admin user
// whose role does not include Acme_Locator::location gets the
// "Sorry, you need permissions to view this content" page instead.
Discussion