Writing reports
Report plugins live in src/Plugin/Report and use the #[Report] attribute. Its permission and roles use the format of a route's _permission and _role requirements: separated by , when all are required, or by + when any one is enough, e.g. permission: 'access site reports,access files overview' or roles: 'editor+site_manager'. Users who have the permissions or the roles can view the report (ReportBase::access()); override access() for other rules. Extend ReportBase and implement either:
RealtimeReportInterface:getRows()builds the rows on every request.GeneratedReportInterface:getBatchOperations()returns plugin methods to run as batch operations. Each is called with its arguments, aReportWriterand the batch context, and adds rows with$writer->addRow($row, $sheet).
A report's page is /admin/reports/<plugin ID>. Choose an ID that no other route uses under /admin/reports, such as core's status or dblog: their routes win, so the report can't be shown. The status report warns about such reports, and they get no menu link.
- Reports can replace other modules' pages with
routesin their attribute: route names, each with the sheet it shows, e.g.routes: ['entity.field_storage_config.collection' => self::SHEET_DEFAULT]. The routes keep their path, access requirements, menu links and local tasks, so local tasks on them become the report's sheet tabs. The report's access, including its downloads, follows the requirements of the route that shows its default sheet, instead of itspermission. Routes of modules that aren't installed are skipped.
Base classes
Every report extends ReportBase, directly or through one of the classes below. Pick the most specific one that fits. Classes marked experimental are @internal: they may still change until more reports use them.
The Implements column lists the interfaces that matter when writing a report, including inherited ones: RealtimeReportInterface or GeneratedReportInterface, and PluginFormInterface for an options form.
| Class | Extends | Implements | Use it for | You implement |
|---|---|---|---|---|
ReportBase |
PluginBase |
ReportInterface, ContainerFactoryPluginInterface |
Any report. Handles access, options, URLs and cacheability. | getHeader(), and getRows() (realtime) or getBatchOperations() (generated) |
StatusReportBase |
ReportBase |
RealtimeReportInterface |
Reports shown like core's status report, with type: 'status' in the attribute. |
getRows() |
EntityReportBase (experimental) |
ReportBase |
PluginFormInterface |
Reports that list entities. | Your columns, and getRows() or getBatchOperations() |
FormattedTextReportBase (experimental) |
EntityReportBase |
GeneratedReportInterface, PluginFormInterface |
Reports that check formatted text in content. | getValueHeader() and getHtmlRows() |
LinkReportBase (experimental) |
FormattedTextReportBase |
GeneratedReportInterface, PluginFormInterface |
Reports that check URLs in links and formatted text. | getValueHeader() and getUrlRows() |
StatusReportBase
Status reports (type: 'status') extend StatusReportBase. Its getHeader() sets the columns, so each row from getRows() has the keys key, label, status, severity (a RequirementSeverity) and description.
EntityReportBase
Reports that list entities can extend EntityReportBase (experimental): it adds an include option (unpublished content, content in the trash) and an options form for it (add your own options in buildConfigurationForm() before calling the parent), entity queries and batches that respect them, and the title, type, ID, page and edit URL and state columns (from Helper's EntityHelper::toArray(), with the state from EntityHelper::getState(): deleted, moderation state, published or unpublished).
FormattedTextReportBase and LinkReportBase
Reports that check formatted text in content can extend FormattedTextReportBase (experimental), as HTML elements does: it finds formatted text fields (including field types that extend them) and formatted custom field Text (long) subfields, adds a Check text fields option, batches by field and builds the summary table. Implement getValueHeader() and getHtmlRows().
Reports that also check URLs extend LinkReportBase (experimental), as Absolute URLs does: it adds link and URI fields and custom field link and URI subfields, getUrlRows(), and getHtmlLinks() to find the links in formatted text.
Modules can check other field types, or skip some, with hook_reports_formatted_text_columns_alter() and hook_reports_link_columns_alter(), and other custom field subfields with hook_reports_formatted_text_custom_columns_alter() and hook_reports_link_custom_columns_alter() (see reports.api.php), for every report built on each base class.
Report data
Sheets
Reports with several sheets override getSheets(), which returns the sheet labels keyed by sheet ID; reports with one sheet use ReportInterface::SHEET_DEFAULT. getHeader($sheet), getRows($sheet) and the report writer's addRow($row, $sheet) take the same sheet IDs.
Reports meant to be read as a spreadsheet, such as ones with many sheets, can set downloadOnly: TRUE in their attribute: their page lists the sheets with their row counts and offers the Excel file, instead of showing a table. Only the Excel file is kept: the sheets' CSV files are deleted once it's written.
Columns
getHeader($sheet) returns a sheet's columns, keyed by column key. Each is a label, or an array with these keys:
| Key | Value | What it does |
|---|---|---|
label |
String | The column heading. |
filter |
'list', 'search' or FALSE (the default) |
A filter above the table: 'list' is a dropdown of the column's values, for columns with few different values, and 'search' a text box. Most columns don't need one: the filter bar's Search field already searches every column. |
url |
A column key | Links the column's text to another column's URL. |
hidden |
TRUE |
Leaves the column out of the page, but keeps it in downloads and Drush. |
list |
TRUE, or a list style such as 'comma-list' |
Marks a column whose cells are lists (arrays), shown as bulleted lists or in the given style. Set it on every such column, since generated reports need it to keep them as lists. |
Cells
Row cells are keyed by column key. Each kind of value is shown like this:
| Value | Page | CSV | Excel | Drush |
|---|---|---|---|---|
| String or number | As it is; absolute http(s) URLs become links |
As it is | As it is; absolute http(s) URLs become HYPERLINK() formulas (up to Excel's 255 character limit) |
As it is |
Url object |
A link | The absolute URL | A HYPERLINK() formula |
The absolute URL |
Link object |
A link | Its text | Its text | Its text |
Array, in a column with the list option |
A bulleted list, even for one item, or the column's list style | One item per line | One item per line, in a wrapped cell | An array in JSON and YAML; one item per line in table and CSV output |
- To keep a
Link's URL in downloads, put the URL in its own column and point to it with the link column'surloption. - Arrays in columns without the
listoption are shown as comma lists. - Markup, e.g.
TranslatableMarkup, is plain text, without HTML tags. Booleans are1or0, enums their case name, andNULLis empty.
Generated reports store cells as text, with lists one item per line, so their pages show what the CSV holds: links for absolute URLs, and line breaks in text. List columns are split back into arrays when they're read.
Options, summary, requirements, and cacheability
- Reports take options through
defaultConfiguration(), and implementPluginFormInterfaceto show a form above the report. Generated reports are stored separately for each configuration. Options come from the query string and Drush, so validate them insetConfiguration(). Store list options as comma-separated strings withnormalizeList()and read them withgetListOption(): the query string drops empty arrays, so an emptied list would otherwise fall back to its default. getSummary()returns a render array describing what the report checks, e.g. the entity types, bundles and fields it scans, rather than its current options. It's shown on the report page, and defaults to the report's description.- The static
isApplicable(array $plugin_definition)returns FALSE when the report doesn't apply to the site, e.g. an optional module it reports on isn't installed: it then has no menu link under Reports, and its pages return 404 Not Found. getRequirements()returns requirements inhook_runtime_requirements()'s format, e.g. a warning when there's nothing to report on. Errors prevent viewing and generating the report; warnings are shown with it. Storage for generated reports is checked for you, and is the only requirement on the status report.toUrl($rel, $options)returns the URL of the report's page (canonical), downloads (csv,xlsx) or generation (generate), with its current options.getOperations()adds buttons to the report page, next to the downloads (links the user can't access are hidden).getCacheTags()lists the data a report is built from. Prefer list tags, such as bundle list tags (node_list:article), to tags of individual entities.