Skip to content

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, a ReportWriter and 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 routes in 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 its permission. 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's url option.
  • Arrays in columns without the list option are shown as comma lists.
  • Markup, e.g. TranslatableMarkup, is plain text, without HTML tags. Booleans are 1 or 0, enums their case name, and NULL is 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 implement PluginFormInterface to 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 in setConfiguration(). Store list options as comma-separated strings with normalizeList() and read them with getListOption(): 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 in hook_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.