Extending
A plugin knows better than Microscope does what “misconfigured” looks like for it, and its findings belong in the same report — scored the same way, printed on the same page, emailed in the same digest.
Registering a check
use justinholtweb\microscope\events\RegisterChecksEvent;
use justinholtweb\microscope\services\Checks;
use yii\base\Event;
Event::on(Checks::class, Checks::EVENT_REGISTER_CHECKS, function(RegisterChecksEvent $event) {
$event->checks[] = new MyCheck();
});
Writing one
A check implements CheckInterface: it describes itself, says whether it can run in this environment, and returns findings. Extending BaseCheck gives you sensible defaults plus critical(), warning(), notice() and pass().
use justinholtweb\microscope\checks\BaseCheck;
use justinholtweb\microscope\checks\CheckContext;
use justinholtweb\microscope\models\Category;
class MyCheck extends BaseCheck
{
public function handle(): string
{
return 'myPluginIndexFreshness';
}
public function category(): string
{
return Category::DATABASE;
}
public function label(): string
{
return 'Search index freshness';
}
public function description(): string
{
return 'Checks how far behind the search index has fallen.';
}
public function isAvailable(CheckContext $context): bool
{
return MyPlugin::getInstance()->getSettings()->indexingEnabled;
}
public function unavailableReason(CheckContext $context): string
{
return 'Indexing is switched off, so there is nothing to measure.';
}
public function run(CheckContext $context): array
{
$lag = MyPlugin::getInstance()->index->secondsBehind();
if ($lag < 300) {
return [$this->pass('Search index is current', sprintf('%ds behind', $lag))];
}
return [
$this->warning([
'title' => 'Search index has fallen behind',
'summary' => 'Queries are being answered from stale data.',
'observed' => sprintf('%d minutes behind', $lag / 60),
'expected' => 'under 5 minutes',
'impact' => 'Visitors see results that no longer match the content...',
'remediation' => "Run `php craft my-plugin/index/rebuild`, then make sure the queue...",
]),
];
}
}
What a good check looks like
- Return passes, not silence. A report that says “the index is current” is more useful than one that merely fails to mention it.
- Skip rather than guess. If
isAvailable()returns false, the check is recorded as skipped with your reason and excluded from the score — which is the honest outcome when you can’t measure something. See Scores and findings. - Write the remediation you’d want to receive.
impactandremediationare Markdown and can carry fenced code blocks. The steps, the snippet and how to verify it worked are the whole point. - Pick a stable handle. It goes into settings, into the finding’s fingerprint and onto the CLI. Changing it later makes every existing finding look new.
- Use the probes. Checks are given a
CheckContextcarryingphp,database,serverandcraftprobes rather than reaching forCraft::$apporini_get(). The probes memoize, so ten checks readingSHOW VARIABLEScost one query between them — and a probe can be substituted in a test, so your thresholds can be tested against a fabricated server without one existing. - Respect the environment.
$context->isProduction()tells you whether to hold the site to production standards.devModeis supposed to be on locally, and telling a developer off for it on every scan trains them to ignore the report.
Severities
Findings cost the site score: a critical 25, a warning 10, a notice 3. Be proportionate — a check that returns a critical for something recoverable will drag a site to an F and make the whole report less credible. BaseCheck::severityForRatio() is there for the common case of grading a measured value against a target.
Cancelling a scan
Scans::EVENT_BEFORE_SCAN is cancellable, which is the hook for suppressing scheduled scans during a deploy window — when every finding would describe a site mid-deploy:
use justinholtweb\microscope\events\ScanEvent;
use justinholtweb\microscope\services\Scans;
use yii\base\Event;
Event::on(Scans::class, Scans::EVENT_BEFORE_SCAN, function(ScanEvent $event) {
if (App::env('DEPLOY_IN_PROGRESS')) {
$event->isValid = false;
}
});
Scans::EVENT_AFTER_SCAN fires once the scan is stored, carrying the ScanResult and the saved scan’s ID — the place to post a score to Slack, or to fail your own pipeline on a condition --fail-on doesn’t express.