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 CheckInterface . The container tags every implementation automatically, so there is no registration list to edit:

EXT:my_extension/Classes/GoLive/CookieBannerCheck.php
<?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())],
        );
    }
}
Copied!

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/Services.yaml. Nothing else:

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/*'
Copied!

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/Services.php, which is evaluated while the container is built:

EXT:my_extension/Configuration/Services.php
<?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');
};
Copied!

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 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.

What the context offers 

CheckContext 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() .

getSite() , getSiteIdentifier() , getSiteConfiguration() , getSetting()
The site being checked and its configuration.
hasLiveUrl() , getBaseUrl() , 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.
fetch() , fetchAbsolute() , 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.
recordEditUri() , pageLayoutUri() , siteConfigurationUri() , moduleUri()
Targets for CheckLink , so the report can point at the record, the page, or the site configuration instead of only describing the problem.

Texts 

The texts of a check live in the language file of its own extension, at 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:

EXT:my_extension/Resources/Private/Language/locallang_golive.xlf
<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>
Copied!

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.locallang_golive.xlf.

A fix button 

Implement FixableCheckInterface 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';
}
Copied!

The returned key is the success message. applyFix() 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.