.. include:: /Includes.rst.txt
.. _extending:
=====================
Adding your own check
=====================
Target group: **Developers**
A check is one class. Any extension can contribute checks to the same report,
and this extension does not need to know about them.
.. _extending-class:
The class
=========
Implement :php:`CheckInterface`. The container tags every implementation
automatically, so there is no registration list to edit:
.. code-block:: php
:caption: EXT:my_extension/Classes/GoLive/CookieBannerCheck.php
hasLiveUrl()) {
return CheckResult::skipped('skip.noLiveUrl');
}
$response = $context->fetch('/');
if (!$response->reachable) {
return CheckResult::skipped('skip.unreachable', [$response->uri, $response->error]);
}
if (str_contains($response->body, 'data-consent')) {
return CheckResult::ok('check.cookieBanner.ok');
}
return CheckResult::failed(
'check.cookieBanner.fail',
'check.cookieBanner.solution',
details: [$response->uri],
links: [new CheckLink('link.siteConfiguration', $context->siteConfigurationUri())],
);
}
}
The identifier ends up in the translation keys, in the DOM anchor and in the fix
request, so it must not change after the check has been shipped.
.. _extending-registration:
Registration
============
Your extension needs the same :yaml:`_instanceof` block in its own
:file:`Configuration/Services.yaml`. Nothing else:
.. code-block:: yaml
:caption: EXT:my_extension/Configuration/Services.yaml
services:
_defaults:
autowire: true
autoconfigure: true
public: false
_instanceof:
WebagenturYahya\GoLiveCheck\CheckInterface:
tags: ['golive.check']
MyVendor\MyExtension\:
resource: '../Classes/*'
This assumes your extension requires ``webagentur-yahya/golive-check``.
If it has to work without it as well, exclude the check classes from the YAML —
both from the :yaml:`_instanceof` block and from the resource glob that would
otherwise autowire them — and register them behind a guard in
:file:`Configuration/Services.php`, which is evaluated while the container is
built:
.. code-block:: php
:caption: EXT:my_extension/Configuration/Services.php
services()
->set(CookieBannerCheck::class)
->autowire()
->tag('golive.check');
};
.. _extending-results:
The three results
=================
.. rst-class:: dl-parameters
:php:`CheckResult::ok()`
Passed. May carry details, which are shown as the confirmed value.
:php:`CheckResult::failed()`
A finding. Takes a message key and a solution key, optional arguments for
the message, details, links, and a severity that overrides the check's own
for this one finding.
:php:`CheckResult::skipped()`
The answer could not be reached. Use this whenever the check would otherwise
have to guess — an unreachable URL, a missing table, an extension that is
not installed. Never return :php:`ok()` in that situation: a false green
replaces the very check it pretends to be.
The ``$details`` are the raw values that were found and are never translated.
They are what makes a finding actionable, and translating them would stop the
report from showing what is really in the configuration.
.. _extending-context:
What the context offers
=======================
:php:`CheckContext` is handed to :php:`run()` so that a check does not fetch
site, request and URI builder itself. That is also why a check can be unit
tested without a TYPO3 bootstrap — build the context by hand and call
:php:`run()`.
.. rst-class:: dl-parameters
:php:`getSite()`, :php:`getSiteIdentifier()`, :php:`getSiteConfiguration()`, :php:`getSetting()`
The site being checked and its configuration.
:php:`hasLiveUrl()`, :php:`getBaseUrl()`, :php:`isBaseUrlAssumed()`
The resolved live address without a trailing slash. It is *assumed* when the
site has a relative base and the host had to be taken from the current
backend request — a finding based on it would not hold, so most checks skip
then.
:php:`fetch()`, :php:`fetchAbsolute()`, :php:`url()`
HTTP requests against the site, with a five second timeout, at most 256 KB
of body, and 4xx or 5xx returned as a result rather than thrown. Each URL is
fetched once per report, so several checks can read the same file.
:php:`recordEditUri()`, :php:`pageLayoutUri()`, :php:`siteConfigurationUri()`, :php:`moduleUri()`
Targets for :php:`CheckLink`, so the report can point at the record, the
page, or the site configuration instead of only describing the problem.
.. _extending-labels:
Texts
=====
The texts of a check live in the language file of *its own* extension, at
:file:`Resources/Private/Language/locallang_golive.xlf`. Which extension that is
gets answered by the file the class lives in, so a check declares nothing and
inherits nothing.
Required for every check:
.. code-block:: xml
:caption: EXT:my_extension/Resources/Private/Language/locallang_golive.xlf
Consent is asked before trackingOne sentence on what it costs to get this wrong.
Plus every message and solution key the check returns — above:
``check.cookieBanner.ok``, ``check.cookieBanner.fail`` and
``check.cookieBanner.solution``. Placeholders are ``%s``, filled from the
message arguments in order.
The shared keys are found without you copying them: ``skip.*``, ``link.*``,
``severity.*`` and ``status.*`` fall back to this extension's language file. A
key that resolves nowhere is shown verbatim in the report, which makes a
forgotten text visible immediately rather than silently empty.
A translation into another language is a second file next to it, prefixed with
the language key — :file:`de.locallang_golive.xlf`.
.. _extending-fix:
A fix button
============
Implement :php:`FixableCheckInterface` as well — but only if the correct value
follows necessarily from the configuration:
.. code-block:: php
public function getFixLabelKey(): string
{
return 'check.cookieBanner.fixLabel';
}
public function applyFix(CheckContext $context): string
{
// Throw \RuntimeException if it cannot be applied after all.
return 'check.cookieBanner.fixed';
}
The returned key is the success message. :php:`applyFix()` may assume that
:php:`run()` has just reported a finding; the controller verifies that before
calling.
Everything where a human's intention is involved keeps the link to the place
instead. A button that quietly writes a guess into a configuration is worse than
none, because afterwards nobody looks again.