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.
The class
Implement
Check. The container tags every implementation
automatically, so there is no registration list to edit:
<?php
declare(strict_types=1);
namespace MyVendor\MyExtension\GoLive;
use WebagenturYahya\GoLiveCheck\CheckCategory;
use WebagenturYahya\GoLiveCheck\CheckContext;
use WebagenturYahya\GoLiveCheck\CheckInterface;
use WebagenturYahya\GoLiveCheck\CheckLink;
use WebagenturYahya\GoLiveCheck\CheckResult;
use WebagenturYahya\GoLiveCheck\CheckSeverity;
final class CookieBannerCheck implements CheckInterface
{
public function getIdentifier(): string
{
return 'cookieBanner';
}
public function getCategory(): CheckCategory
{
return CheckCategory::Legal;
}
public function getSeverity(): CheckSeverity
{
return CheckSeverity::Blocker;
}
public function run(CheckContext $context): CheckResult
{
if (!$context->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.
Registration
Your extension needs the same
_instanceof
block in its own
Configuration/. Nothing else:
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
_instanceof
block and from the resource glob that would
otherwise autowire them — and register them behind a guard in
Configuration/, which is evaluated while the container is
built:
<?php
declare(strict_types=1);
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
use WebagenturYahya\GoLiveCheck\CheckInterface;
use MyVendor\MyExtension\GoLive\CookieBannerCheck;
return static function (ContainerConfigurator $configurator): void {
if (!interface_exists(CheckInterface::class)) {
return;
}
$configurator->services()
->set(CookieBannerCheck::class)
->autowire()
->tag('golive.check');
};
The three results
CheckResult:: ok () - Passed. May carry details, which are shown as the confirmed value.
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.
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
okin 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.
What the context offers
Check is handed to
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
run.
get,Site () get,Site Identifier () get,Site Configuration () getSetting () - The site being checked and its configuration.
has,Live Url () get,Base Url () isBase Url Assumed () - 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.
fetch,() fetch,Absolute () 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.
record,Edit Uri () page,Layout Uri () site,Configuration Uri () moduleUri () - Targets for
Check, so the report can point at the record, the page, or the site configuration instead of only describing the problem.Link
Texts
The texts of a check live in the language file of its own extension, at
Resources/. 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:
<trans-unit id="check.cookieBanner.title">
<source>Consent is asked before tracking</source>
</trans-unit>
<trans-unit id="check.cookieBanner.why">
<source>One sentence on what it costs to get this wrong.</source>
</trans-unit>
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 — de..
A fix button
Implement
Fixable as well — but only if the correct value
follows necessarily from the configuration:
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.
apply may assume that
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.