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